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

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ư HStack
nơ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")
}
SwiftUI
cung 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 BalancedHStack
phù hợp Layout
. Giao Layout
thức yêu cầu hai phương pháp:
sizeThatFits
: Cung cấp tổngCGSize
container cho hệ thốngplaceSubviews
: Để cập nhật vị trí của trẻ em trong giới hạn được cung cấp- Công cụ bố cục đơn giản
- Cách trình bày
- ViewThatPhù hợp
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()
}
}