Layout personalizzato SwiftUI con Simple Layout Engine

Nov 28 2022
La matematica richiesta per il layout personalizzato di SwiftUI mi ricorda i giorni prima di AutoLayout e del sistema basato sui vincoli. La cosa buona è che Simple Layout Engine fornisce già un bel sistema per gestire tutta la matematica coinvolta.
Foto di https://burst.shopify.com/@sarahpflugphoto

La matematica richiesta per il layout personalizzato di SwiftUI mi ricorda i giorni prima di AutoLayout e del sistema basato sui vincoli. La cosa buona è che Simple Layout Engine fornisce già un bel sistema per gestire tutta la matematica coinvolta. Per dimostrare, utilizzerei build the subset of the demo app dalla sessione WWDC su questo argomento: Compose custom layouts with SwiftUI .

Problema

L'idea è di avere una vista contenitore simile a quella in HStackcui ogni bambino ha la stessa larghezza, ma con l'eccezione che la larghezza dovrebbe essere quella massima di un bambino. Questo è il modo in cui HStackposiziona i bambini per impostazione predefinita.

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

      
                

Quello che vogliamo veramente è avere qualcosa che assomigli a width = max(children.width) , che avrebbe tutti i bambini con la larghezza uguale a qualunque sia la larghezza del bel testo

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

      
                

SwiftUIfornisce un modo per collegarsi al sistema di layout per fornire tutta la matematica personalizzata. Per il nostro caso possiamo creare un BalancedHStackfile conforme al Layoutprotocollo. Il Layoutprotocollo richiede due metodi:

  1. sizeThatFits: Per fornire al CGSizesistema il totale del contenitore
  2. placeSubviews: Per aggiornare le posizioni dei bambini entro i limiti forniti
  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. Motore di layout semplice
  5. Disposizione
  6. ViewThatFits