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

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
sizeThatFits
:CGSize
컨테이너 전체를 시스템에 제공하기 위해placeSubviews
: 제공된 경계 내에서 자식의 위치를 업데이트하기 위해- 간단한 레이아웃 엔진
- 형세
- 맞는 보기
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()
}
}