Simple Layout Engine을 사용한 SwiftUI 사용자 정의 레이아웃

Nov 28 2022
SwiftUI 사용자 정의 레이아웃에 필요한 수학은 AutoLayout 및 제약 조건 기반 시스템 이전의 일을 상기시킵니다. 좋은 점은 Simple Layout Engine이 관련된 모든 수학을 처리할 수 있는 멋진 시스템을 이미 제공하고 있다는 것입니다.
https://burst.shopify.com/@sarahpflugphoto의 사진

SwiftUI 사용자 정의 레이아웃에 필요한 수학은 AutoLayout 및 제약 조건 기반 시스템 이전의 일을 상기시킵니다. 좋은 점은 Simple Layout Engine 이 관련된 모든 수학을 처리할 수 있는 멋진 시스템을 이미 제공하고 있다는 것입니다. 시연을 위해 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. 맞는 보기