Trang chủ

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.