รูปแบบที่กำหนดเอง SwiftUI ด้วย Simple Layout Engine
คณิตศาสตร์ที่จำเป็นสำหรับเค้าโครงแบบกำหนดเองของ SwiftUI ทำให้ฉันนึกถึงวันก่อน AutoLayout และระบบที่ใช้ข้อจำกัด สิ่งที่ดีคือSimple Layout Engineมีระบบที่ดีในการจัดการคณิตศาสตร์ทั้งหมดที่เกี่ยวข้องอยู่แล้ว เพื่อสาธิต ฉันจะใช้ build ชุดย่อยของแอปสาธิตจากเซสชัน 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()
}
}