รูปแบบที่กำหนดเอง SwiftUI ด้วย Simple Layout Engine

Nov 28 2022
คณิตศาสตร์ที่จำเป็นสำหรับเค้าโครงแบบกำหนดเองของ SwiftUI ทำให้ฉันนึกถึงวันก่อน AutoLayout และระบบที่ใช้ข้อจำกัด สิ่งที่ดีคือ Simple Layout Engine มีระบบที่ดีในการจัดการคณิตศาสตร์ทั้งหมดที่เกี่ยวข้องอยู่แล้ว
รูปภาพโดย https://burst.shopify.com/@sarahpflugphoto

คณิตศาสตร์ที่จำเป็นสำหรับเค้าโครงแบบกำหนดเองของ SwiftUI ทำให้ฉันนึกถึงวันก่อน AutoLayout และระบบที่ใช้ข้อจำกัด สิ่งที่ดีคือSimple Layout Engineมีระบบที่ดีในการจัดการคณิตศาสตร์ทั้งหมดที่เกี่ยวข้องอยู่แล้ว เพื่อสาธิต ฉันจะใช้ build ชุดย่อยของแอปสาธิตจากเซสชัน WWDC ในหัวข้อนี้: เขียนโครงร่างแบบกำหนดเอง ด้วยSwiftUI

ปัญหา

แนวคิดคือการมีมุมมองคอนเทนเนอร์คล้ายกับHStackที่เด็กทุกคนมีความกว้างเท่ากัน แต่มีข้อยกเว้นว่าความกว้างควรเป็นความกว้างสูงสุดที่เด็กมี นี่คือวิธีการHStackวางเด็กไว้โดยปริยาย

HStack {
    WLText("hi")
    WLText("!")
    WLText("beautiful")
    WLText("world")
}

      
                

สิ่งที่เราต้องการจริง ๆ คือมีบางอย่างที่ดูเหมือนwidth = max(children.width)ซึ่งจะให้เด็กทุกคนมีความกว้างเท่ากับความกว้างของข้อความที่สวยงาม

BalancedHStack {
    WLText("hi")
    WLText("!")
    WLText("beautiful")
    WLText("world")
}

      
                

SwiftUIให้วิธีการเชื่อมต่อกับระบบเค้าโครงเพื่อให้คณิตศาสตร์ที่กำหนดเองทั้งหมด สำหรับกรณีของเรา เราสามารถสร้างBalancedHStackที่สอดคล้องกับLayoutโปรโตคอลได้ โปรโตคอลLayoutต้องการสองวิธี:

  1. sizeThatFits: เพื่อจัดเตรียมผลรวมCGSizeของคอนเทนเนอร์ให้กับระบบ
  2. placeSubviews: เพื่ออัพเดทตำแหน่งของลูกภายในขอบเขตที่กำหนด
  3. struct BalancedHStack: Layout {
      func sizeThatFits(proposal: ProposedViewSize,
                        subviews: Subviews,
                        cache: inout ()) -> CGSize {
        fatalError()
        // TODO
      }
    
      func placeSubviews(in bounds: CGRect,
                         proposal: ProposedViewSize,
                         subviews: Subviews,
                         cache: inout ()) {
        // TODO
      }
    }
    

    struct BalancedHStack: Layout {
    
      struct CacheData {
        let childSize: CGSize
        let distances: [CGFloat]
      }
    
      func makeCache(subviews: Subviews) -> CacheData {
        let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let width = subviewSizes.map { $0.width }.max() ?? 0
        let height = subviewSizes.map { $0.height }.max() ?? 0
        let distances: [CGFloat] = (0..<subviews.count).map { idx in
          guard idx < subviews.count - 1 else { return 0 }
          return subviews[idx].spacing.distance(to: subviews[idx + 1].spacing, along: .horizontal)
        }
        return CacheData(
          childSize: CGSize(width: width, height: height),
          distances: distances
        )
      }
    
      // ...
    }
    

    func sizeThatFits(proposal: ProposedViewSize,
                        subviews: Subviews,
                        cache: inout CacheData) -> CGSize {
        let totalDistance = cache.distances.reduce(0, +)
        return CGSize(
          width: cache.childSize.width * CGFloat(subviews.count) + totalDistance,
          height: cache.childSize.height
        )
      }
    

    func placeSubviews(in bounds: CGRect,
                         proposal: ProposedViewSize,
                         subviews: Subviews,
                         cache: inout CacheData) {
        let layout = SLELayout(parentFrame: bounds, direction: .row, alignment: .center)
        do {
          var items: [SLEItem] = []
          for idx in 0..<subviews.count {
            items.append(try layout.add(item: .size(cache.childSize)))
            try layout.add(item: .width(cache.distances[idx]))
          }
    
          for (idx, subview) in subviews.enumerated() {
            subview.place(
              at: try items[idx].frame().origin,
              proposal: ProposedViewSize(cache.childSize)
            )
          }
        }
        catch { print("Unable to layout \(error)") }
      }
    
           
                    

    extension SLEDirection {
      var axis: Axis {
        switch self {
        case .row: return .horizontal
        case .column: return .vertical
        }
      }
    }
    
    struct BalancedStack: Layout {
    
      let direction: SLEDirection
    
      init(_ direction: SLEDirection) {
        self.direction = direction
      }
    
      struct CacheData {
        let childSize: CGSize
        let distances: [CGFloat]
      }
    
      func makeCache(subviews: Subviews) -> CacheData {
        let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let width = subviewSizes.map { $0.width }.max() ?? 0
        let height = subviewSizes.map { $0.height }.max() ?? 0
        let distances: [CGFloat] = (0..<subviews.count).map { idx in
          guard idx < subviews.count - 1 else { return 0 }
          return subviews[idx].spacing.distance(to: subviews[idx + 1].spacing, along: direction.axis)
        }
        return CacheData(
          childSize: CGSize(width: width, height: height),
          distances: distances
        )
      }
    
      func sizeThatFits(proposal: ProposedViewSize,
                        subviews: Subviews,
                        cache: inout CacheData) -> CGSize {
        let totalDistance = cache.distances.reduce(0, +)
        let containerWidth: CGFloat
        let containerHeight: CGFloat
        switch direction {
        case .row:
          containerWidth = cache.childSize.width * CGFloat(subviews.count) + totalDistance
          containerHeight = cache.childSize.height
        case .column:
          containerWidth = cache.childSize.width
          containerHeight = cache.childSize.height * CGFloat(subviews.count) + totalDistance
        }
    
        return CGSize(width: containerWidth, height: containerHeight)
      }
    
      func placeSubviews(in bounds: CGRect,
                         proposal: ProposedViewSize,
                         subviews: Subviews,
                         cache: inout CacheData) {
        let layout = SLELayout(parentFrame: bounds, direction: direction, alignment: .center)
        do {
          var items: [SLEItem] = []
          for idx in 0..<subviews.count {
            items.append(try layout.add(item: .size(cache.childSize)))
            try layout.add(item: .width(cache.distances[idx]))
          }
    
          for (idx, subview) in subviews.enumerated() {
            subview.place(
              at: try items[idx].frame().origin,
              proposal: ProposedViewSize(cache.childSize)
            )
          }
        }
        catch { print("Unable to layout \(error)") }
      }
    }
    
           
                    

    struct TextList: View {
      var body: some View {
        WLText("hi")
        WLText("!")
        WLText("beautiful")
        WLText("world")
      }
    }
    ViewThatFits {
        BalancedStack(.row) {
            TextList()
        }
    
        BalancedStack(.column) {
            TextList()
        }
    }
    

  4. เอ็นจิ้นเค้าโครงอย่างง่าย
  5. เค้าโครง
  6. ดูนั่นพอดี