Ghép lại thay vì cấu hình
Nhiều kiểu nhỏ, bọc vào nhau, thay cho một kiểu với năm mươi thuộc tính
UILabel của UIKit có chừng bốn mươi thuộc tính cấu hình được. Text của SwiftUI không có cái nào
— mọi câu hỏi bạn định trả lời bằng một thuộc tính đều được trả lời bằng cách bọc Text vào một
thứ khác.
Text("Xin chào")
.font(.headline)
.foregroundStyle(.secondary)
.padding(.horizontal, 12)
Không dòng nào trong bốn dòng đó sửa cái Text. Mỗi dòng trả về một giá trị mới bọc lấy giá trị
trước, và thứ đến được bộ dựng hình là một cái tổ sâu bốn lớp. Đó là toàn bộ cuộc đánh đổi: một bề
mặt API lớn trên một kiểu được thay bằng một bề mặt API nhỏ trên nhiều kiểu khớp được vào nhau.
Vì sao đánh đổi như vậy là đáng
Cái lợi dễ thấy là hành vi ghép được với nhau mà framework không cần đoán trước tổ hợp của bạn.
.padding() không được viết ra với Text trong đầu; nó chạy được trên bất cứ thứ gì, kể cả những
view chưa tồn tại lúc nó được viết, kể cả view của bạn.
Cái lợi tinh tế hơn là không có trạng thái không hợp lệ nào để bạn cấu hình mà lạc vào. Một UILabel
với numberOfLines = 0, chiều cao cố định và adjustsFontSizeToFitWidth bật lên là một đối tượng
có thật ở trạng thái mâu thuẫn, và cái class ấy phải quyết định làm gì với nó. Một view SwiftUI không
có trạng thái mâu thuẫn vì nó không có trạng thái — nó là một giá trị, và mâu thuẫn chỉ có thể tồn
tại dưới dạng hai lớp bọc bất đồng với nhau, thứ được giải quyết bằng các quy tắc bố cục chứ không
phải bằng một bảng thứ tự ưu tiên thuộc tính mà chẳng ai nhớ nổi.
Mẹo
Phép thử xem bạn đã hiểu chưa: .font(.title) áp lên một VStack thì chạy, và đó không phải
trường hợp đặc biệt. Nó ghi vào environment, và mọi Text bên dưới đọc lấy. Một modifier không
phải “một cái setter cho view nằm bên trái nó”.
@ViewBuilder, và thứ nó thật sự dựng ra
Những closure bạn viết trong body không phải closure thông thường. @ViewBuilder là một result
builder biến một dãy câu lệnh thành một giá trị lồng nhau duy nhất:
VStack {
Text("Tiêu đề")
Text("Phụ đề")
}
Cái đó không tạo ra một mảng các view. Nó tạo ra VStack<TupleView<(Text, Text)>> — một kiểu mã
hóa đúng hai con thuộc đúng những kiểu ấy, chốt xong ngay lúc biên dịch.
Đây là lý do các tên kiểu dài ra, và là lý do trước kia trình biên dịch hay kêu ca khi có mười con: mỗi con là một tham số generic. Đây cũng là lý do view rẻ. Không có mảng nào phải cấp phát, không có lưu lượng trên heap, không có điều phối động nào để với tới các con.
Một if trong builder trở thành _ConditionalContent<TrueBranch, FalseBranch>, điều này quan trọng
hơn vẻ ngoài của nó — đó là lý do hai nhánh là hai view khác nhau với danh tính khác nhau, đúng
cái điểm mà cả chương sau nói về.
Tách một view con ra không tốn gì
Vì một view là một giá trị và body của nó chỉ được đọc khi cần, việc chẻ một body to thành các
view nhỏ hơn là miễn phí. Đây là điều ngược với bản năng UIKit, nơi thêm một view nghĩa là thêm một
đối tượng, thêm một lớp và thêm một thứ phải bố cục.
struct OrderRow: View {
let order: Order
var body: some View {
HStack {
OrderBadge(status: order.status)
VStack(alignment: .leading) {
Text(order.title)
Text(order.placedAt, format: .dateTime)
}
}
}
}
Có một lập luận hiệu năng thật sự cho việc này, không chỉ là gọn gàng: SwiftUI làm mất hiệu lực ở mức
từng view. Nếu tất cả nằm trong một body khổng lồ thì mọi thay đổi đều chạy lại toàn bộ. Chẻ ra
thành OrderBadge và phần còn lại, một thay đổi trạng thái chỉ chạy lại riêng OrderBadge.body.
Cảnh báo
Hãy ưu tiên một struct tuân thủ View hơn là một thuộc tính var someSection: some View hay
một hàm @ViewBuilder. Thuộc tính tính toán và hàm bị nội tuyến vào body của view cha nên
không có danh tính, không có bộ nhớ riêng và không được làm mất hiệu lực riêng — nghĩa là bạn
giữ được sự gọn gàng và vứt đi phần lợi về hiệu năng.
Chỗ nó thôi có lãi
Việc ghép lại có cái giá của nó, và thành thật về cái giá đó thì đỡ phải cãi nhau với framework về sau.
View bọc quá sâu làm tên kiểu dài khủng khiếp, khiến một lỗi biên dịch nằm sau ba mươi modifier trở nên không đọc nổi. Cách chữa là tách view con ra, thứ dù sao bạn cũng nên làm.
Nghiêm trọng hơn, không phải thứ gì cũng ghép được. Một số modifier phải nằm ngoài cùng mới chạy
(.searchable cần tìm thấy một vùng điều hướng ở phía trên nó), một số chỉ chạy khi là con trực tiếp
của một view cha cụ thể (.listRowInsets đặt ngoài List bị lờ đi lặng lẽ), và vài cái nhạy với
thứ tự theo kiểu không quy tắc nào đoán được. Khi một modifier không làm gì cả, giả thuyết đầu tiên
nên là nó nằm sai chỗ trong cái tổ, chứ không phải nó hỏng.