Simple Layout Engine ile SwiftUI özel düzeni

Nov 28 2022
SwiftUI özel düzeni için gereken matematik, bana AutoLayout'tan önceki günleri ve kısıtlamalara dayalı sistemi hatırlatıyor. İyi olan şey, Simple Layout Engine'in ilgili tüm matematiği halletmek için zaten güzel bir sistem sağlamasıdır.
Fotoğraf: https://burst.shopify.com/@sarahpflugphoto

SwiftUI özel düzeni için gereken matematik, bana AutoLayout'tan önceki günleri ve kısıtlamalara dayalı sistemi hatırlatıyor. İyi olan şey, Simple Layout Engine'in ilgili tüm matematiği halletmek için zaten güzel bir sistem sağlamasıdır. Göstermek için şu konudaki WWDC oturumundaki demo uygulamasının alt kümesini oluşturacağım: Compose custom layouts with SwiftUI .

Sorun

Buradaki fikir, her çocuğun aynı genişliğe sahip olduğu duruma benzer bir kapsayıcı görünüme sahip olmaktır HStack, ancak genişliğin bir çocuğun sahip olduğu maksimum genişlik olması dışında. HStackÇocukları varsayılan olarak bu şekilde yerleştirir.

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

      
                

Aslında istediğimiz, güzel metnin genişliği ne olursa olsun, tüm çocukların genişliğe eşit olmasını sağlayan, width = max(children.width) gibi görünen bir şeye sahip olmaktır.

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

      
                

SwiftUItüm özel matematiği sağlamak için düzen sistemine bağlanmanın bir yolunu sağlar. Bizim durumumuz için protokole BalancedHStackuyan bir tane oluşturabiliriz. LayoutProtokol Layoutiki yöntem gerektirir:

  1. sizeThatFits: Konteynerin toplamını CGSizesisteme sağlamak
  2. placeSubviews: Alt öğelerin konumlarını sağlanan sınırlar içinde güncellemek için
  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. Basit Düzen Motoru
  5. Düzen
  6. Uygun Olanı Görüntüle