Thôi với tới GeometryReader
GeometryReader là công cụ đầu tiên phần lớn mọi người tìm thấy khi một bố cục SwiftUI cần một phép
đo, và nó thường là công cụ sai. Nó có hai đặc tính khiến nó là một lựa chọn mặc định tệ, và phần lớn
những cách dùng truyền thống của nó giờ đã có những thứ thay thế chuyên dụng.
Vì sao nó là lựa chọn mặc định tệ
Nó tham lam. GeometryReader chấp nhận bất cứ kích thước nào được mời, ở cả hai chiều. Bọc một
Text vào trong nó và phần chữ không còn tự lấy kích thước nữa — cái reader nở ra lấp đầy, còn phần
chữ ngồi ở góc trên bên trái của một cái hộp lớn hơn nhiều.
VStack {
GeometryReader { proxy in
Text("Xin chào") // giờ nằm ở góc trên bên trái của toàn bộ chỗ trống
}
Text("Thế giới") // bị đẩy xuống đáy
}
Đây là nguồn gốc của gần như mọi câu hỏi “sao bố cục của tôi lại có một khoảng hở khổng lồ”.
Nó tham gia vào lượt tính kích thước. Nội dung của nó được đo và bố cục ngay trong lúc layout, và
layout chạy nhiều lần trong một khung hình — một stack tra hỏi các con bằng vài lời đề nghị khác
nhau trước khi quyết định bất cứ điều gì. Nằm trong một dòng của List, điều đó nghĩa là body của
reader bị tính đi tính lại trong lúc cuộn, một cách đáng tin cậy để làm cú cuộn bị giật.
Dùng gì thay thế
Cho “lấp đầy container, hoặc một phần của nó”: containerRelativeFrame.
// cũ
GeometryReader { proxy in
Card().frame(width: proxy.size.width * 0.8)
}
// mới
Card().containerRelativeFrame(.horizontal) { width, _ in width * 0.8 }
Không tham lam, không có lượt đo riêng, và nó đọc lên đúng như nghĩa của nó.
Cho “xếp các thứ thẳng hàng qua nhiều dòng”: các đường dẫn căn chỉnh.
Đây là trường hợp người ta dựng bằng GeometryReader cộng một PreferenceKey cộng @State, tức là
ba bộ phận động, một khung hình trễ, và một vòng lặp bố cục tiềm tàng. Các đường dẫn căn chỉnh tùy
biến làm việc đó ngay trong lượt bố cục bình thường:
extension HorizontalAlignment {
private enum ValueColumn: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat { context[.leading] }
}
static let valueColumn = HorizontalAlignment(ValueColumn.self)
}
VStack(alignment: .valueColumn) {
HStack {
Text("Tên")
Text(user.name).alignmentGuide(.valueColumn) { $0[.leading] }
}
HStack {
Text("Địa chỉ email")
Text(user.email).alignmentGuide(.valueColumn) { $0[.leading] }
}
}
Mọi cột giá trị thẳng hàng bất kể nhãn dài ngắn ra sao, không trạng thái và không thêm khung hình trễ nào.
Cho “tôi cần biết kích thước của view này”: onGeometryChange.
Card()
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
cardSize = newSize
}
Cái này đọc hình học mà không đổi bố cục chút nào — view tự lấy kích thước bình thường rồi báo lại
sau. Đó chính xác là thứ mà GeometryReader không bao giờ làm được, và nó dẹp hẳn hành vi tham lam.
Cho vị trí cuộn và các hiệu ứng theo cuộn: các API cuộn.
ScrollView {
…
}
.scrollPosition(id: $scrolledID)
// hiệu ứng theo từng phần tử
Card()
.scrollTransition { content, phase in
content.opacity(phase.isIdentity ? 1 : 0.5)
}
Cái khuôn mẫu tiêu đề parallax vốn cần một GeometryReader báo về một khung trong hệ tọa độ
.global giờ là visualEffect cộng một geometry proxy, không cần chút trạng thái nào.
Mẹo
visualEffect là cái bị đánh giá thấp. Nó cho bạn một geometry proxy để áp các phép biến hình,
độ dịch và độ mờ, nhưng các hiệu ứng đó không tham gia vào bố cục — nên không có gì làm xê dịch
các view quanh nó và không có gì chạy lại lượt bố cục.
Khi nào nó vẫn đúng
Tôi không muốn nói quá. GeometryReader vẫn đúng cho:
- Việc vẽ thật sự cần biết kích thước đầy đủ — một biểu đồ tùy biến, một canvas, một hình nền phải biết biên của nó.
- Đọc một khung trong một hệ tọa độ cụ thể ở chỗ
onGeometryChangekhông vừa, dù khoảng trống này cứ hẹp dần. - Bất cứ thứ gì dù sao cũng nên lấp đầy container của nó, nơi tính tham lam đúng là thứ bạn muốn.
Sự phân biệt: GeometryReader là một container lấp đầy không gian rồi nói cho bạn biết về nó. Hãy
dùng nó khi bạn muốn một container lấp đầy. Đừng dùng nó khi bạn muốn một phép đo, vì một phép đo giờ
là một API khác.
Quy tắc bỏ túi
Trước khi viết GeometryReader, hãy hỏi bạn thật sự cần gì:
| Tôi cần | Dùng |
|---|---|
| Một phần của container | containerRelativeFrame |
| Căn thẳng hàng qua nhiều dòng riêng biệt | AlignmentID tự định nghĩa |
| Kích thước của view này, sau khi bố cục | onGeometryChange |
| Một hiệu ứng thị giác theo cuộn | scrollTransition / visualEffect |
| Đang cuộn tới phần tử nào | scrollPosition(id:) |
| Một cách sắp xếp con tùy biến | protocol Layout |
| Lấp đầy chỗ trống rồi vẽ trong đó | GeometryReader |
Chỉ dòng cuối cùng mới là công cụ mà ai cũng với tới đầu tiên.