Bố cục tùy chỉnh SwiftUI với Simple Layout Engine

Nov 28 2022
Các phép toán cần thiết cho bố cục tùy chỉnh SwiftUI làm tôi nhớ đến những ngày trước AutoLayout và hệ thống dựa trên các ràng buộc. Điều tốt là Simple Layout Engine đã cung cấp một hệ thống tốt để xử lý tất cả các phép toán liên quan.
Ảnh của https://burst.shopify.com/@sarahpflugphoto

Các phép toán cần thiết cho bố cục tùy chỉnh SwiftUI làm tôi nhớ đến những ngày trước AutoLayout và hệ thống dựa trên các ràng buộc. Điều tốt là Simple Layout Engine đã cung cấp một hệ thống tốt để xử lý tất cả các phép toán liên quan. Để chứng minh, tôi sẽ sử dụng xây dựng tập hợp con của ứng dụng demo từ phiên WWDC về chủ đề này: Soạn bố cục tùy chỉnh với SwiftUI .

Vấn đề

Ý tưởng là có chế độ xem vùng chứa tương tự như HStacknơi mọi đứa trẻ có cùng chiều rộng nhưng ngoại trừ chiều rộng phải bằng chiều rộng tối đa mà đứa trẻ có. Đây là cách HStackđặt trẻ em theo mặc định.

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

      
                

Những gì chúng tôi thực sự muốn là có một cái gì đó trông giống như width = max(children.width) , sẽ có tất cả các phần tử con có chiều rộng bằng với bất kỳ chiều rộng nào của văn bản đẹp

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

      
                

SwiftUIcung cấp một cách để cắm vào hệ thống bố trí để cung cấp tất cả các phép toán tùy chỉnh. Đối với trường hợp của chúng tôi, chúng tôi có thể tạo một giao thức BalancedHStackphù hợp Layout. Giao Layoutthức yêu cầu hai phương pháp:

  1. sizeThatFits: Cung cấp tổng CGSizecontainer cho hệ thống
  2. placeSubviews: Để cập nhật vị trí của trẻ em trong giới hạn được cung cấp
  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. Công cụ bố cục đơn giản
  5. Cách trình bày
  6. ViewThatPhù hợp