Tự viết một Layout
Hai phương thức, và cuộc thương lượng trở thành của bạn
Protocol Layout chính là cái protocol mà HStack tuân thủ. Cài đặt nó nghĩa là viết ra hai nửa của
cuộc thương lượng ở các chương trước, lần này từ phía view cha.
protocol Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
subviews: Subviews, cache: inout Cache)
}
sizeThatFits trả lời lời đề nghị của view cha bạn. placeSubviews đặt các con xuống bên trong
khoảng không gian bạn được cho. Đó là toàn bộ protocol.
Một layout kiểu dòng chảy
Trường hợp kinh điển mà SwiftUI không có sẵn: các con xếp từ trái sang phải, xuống dòng mới khi hết bề rộng. Nói cách khác là các chip thẻ.
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews,
cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? .infinity
var rowWidth: CGFloat = 0
var rowHeight: CGFloat = 0
var totalHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if rowWidth > 0, rowWidth + spacing + size.width > maxWidth {
totalHeight += rowHeight + spacing
rowWidth = size.width
rowHeight = size.height
} else {
rowWidth += rowWidth > 0 ? spacing + size.width : size.width
rowHeight = max(rowHeight, size.height)
}
}
return CGSize(width: maxWidth, height: totalHeight + rowHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
subviews: Subviews, cache: inout ()) {
var x = bounds.minX
var y = bounds.minY
var rowHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if x > bounds.minX, x + size.width > bounds.maxX {
x = bounds.minX
y += rowHeight + spacing
rowHeight = 0
}
subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size))
x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
}
}
Dùng như mọi container khác:
FlowLayout(spacing: 8) {
ForEach(tags, id: \.self) { Chip(text: $0) }
}
Hai chi tiết trong đó mới là phần đáng kể. subview.sizeThatFits(.unspecified) là bạn hỏi một đứa
con về kích thước lý tưởng của nó — đúng câu hỏi mà một stack vẫn hỏi. Còn place(at:proposal:) là
bạn đưa ra lời đề nghị sau khi đã quyết định nó nằm ở đâu; đứa con vẫn tự chọn kích thước của mình,
và truyền ProposedViewSize(size) là bạn mời đúng bằng thứ nó vừa xin.
Cảnh báo
Mặc định place(at:) đặt góc trên bên trái của đứa con. Nếu bạn đang đặt mọi thứ quanh một
điểm tâm, hãy truyền anchor: .center thay vì tự trừ đi một nửa kích thước — cái anchor được áp
dụng sau khi đứa con đã chọn, nên phép tính của bạn sẽ dùng nhầm con số.
Cái cache tồn tại là có lý do
Cả sizeThatFits và placeSubviews đều chạy, cả hai đều gọi sizeThatFits trên mọi đứa con, và
toàn bộ chuyện này có thể chạy vài lần trong một khung hình khi một view tổ tiên dò bằng những lời đề
nghị khác nhau. Cái layout ở trên đo mỗi đứa con ít nhất hai lần mỗi lượt.
Với dăm ba cái chip thì miễn phí. Với một trăm cái thì hãy dùng cache:
struct FlowLayout: Layout {
struct Cache {
var sizes: [CGSize]
}
func makeCache(subviews: Subviews) -> Cache {
Cache(sizes: subviews.map { $0.sizeThatFits(.unspecified) })
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
cache.sizes = subviews.map { $0.sizeThatFits(.unspecified) }
}
}
makeCache chạy một lần, updateCache chạy khi các view con thay đổi. Hãy đọc cache.sizes[index]
thay vì đo lại.
Layout value, để cấu hình theo từng con
Một đứa con có thể chuyển thông tin lên cho layout của bạn, và đó chính là cách layoutPriority hoạt
động:
struct Weight: LayoutValueKey {
static let defaultValue: CGFloat = 1
}
extension View {
func weight(_ value: CGFloat) -> some View {
layoutValue(key: Weight.self, value: value)
}
}
Bên trong layout, đọc nó bằng subview[Weight.self]. Đây là cơ chế cho kiểu “để riêng đứa con này
hành xử khác đi” mà không phải thêm một tham số vào container mà mọi đứa con khác đều lờ đi.
Khi nào đừng viết
Layout viết ra thì đã tay, và nó thường không phải câu trả lời.
Nếu một VStack chứa các HStack làm được thì cứ dùng thế. Nếu bạn cần một lưới, Grid đã lo được
cột rộng không đều và ô trải dài. Nếu bạn cần các cột bằng nhau, LazyVGrid có sẵn và nó lười, thứ
mà một Layout tự viết thì không — mọi đứa con của một Layout đều bị tạo ra và đo đạc, nên nó là
công cụ sai cho một nghìn phần tử.
Hãy viết một cái khi quy tắc sắp xếp thật sự là của riêng bạn — dòng chảy, tỏa tròn, xếp gạch,
một trục biểu đồ — và các container có sẵn sẽ phải cần đến GeometryReader cùng một vòng
preference để giả lập. Đó là trường hợp mà Layout thay một khung hình trễ và một đống trạng thái
bằng hai phương thức lương thiện.