Trang chủ

Modifier là lớp bọc

Vì sao thứ tự quan trọng, và cách tự suy ra thay vì học thuộc

.padding() không thêm phần đệm vào một view. Nó trả về một view mới chứa view cũ bên trong và tự báo rằng mình lớn hơn đúng bằng phần đệm. Mọi modifier đều hoạt động như vậy, và một khi điều đó nằm trong đầu bạn, thứ tự modifier thôi là chuyện vụn vặt.

Text("Xin chào").padding().background(.blue)   // màu xanh phủ cả phần đệm
Text("Xin chào").background(.blue).padding()   // màu xanh dừng ở chữ

Hãy đọc chuỗi từ phải sang trái để thấy cái tổ. Ở dòng đầu, background bọc padding(Text), nên nó tô phía sau một thứ có kích thước chữ-cộng-đệm. Ở dòng sau, padding bọc background(Text), nên màu xanh đã được lấy kích thước theo chữ từ trước khi phần đệm được thêm ra bên ngoài nó.

Ở đây không có quy tắc nào phải học thuộc. Cứ vẽ các cái hộp ra là câu trả lời tự rơi xuống.

Ba họ modifier

Không phải modifier nào cũng làm cùng một loại việc, và biết mình đang ở họ nào thì đoán được thứ tự có quan trọng hay không.

Modifier bọc tạo ra một view mới quanh view cũ: padding, frame, background, overlay, border, clipShape. Nhóm này luôn nhạy với thứ tự, vì mỗi cái đều đổi thứ mà cái tiếp theo đang đo.

Circle().frame(width: 100).padding(20)    // tổng 140pt, hình tròn 100
Circle().padding(20).frame(width: 100)    // tổng 100pt, hình tròn 60

Modifier môi trường ghi một giá trị vào environment cho mọi thứ bên dưới: font, foregroundStyle, environment, tint, lineLimit. Nhóm này không nhạy với thứ tự giữa chúng với nhau, và hành xử như CSS kế thừa — cái gần view đang đọc nhất thì thắng.

VStack {
    Text("Kế thừa")
    Text("Ghi đè").font(.caption)
}
.font(.title)

Modifier hành vi gắn thứ gì đó vào một view mà không đổi hình học của nó: onTapGesture, task, onChange, accessibilityLabel, disabled. Thứ tự giữa chúng hiếm khi quan trọng, nhưng thứ tự so với một modifier bọc thì thường có — và đó là nguồn gốc của cái “lỗi” SwiftUI được báo nhiều nhất:

Text("Bấm đi").onTapGesture { … }.padding()   // phần đệm KHÔNG bấm được
Text("Bấm đi").padding().onTapGesture { … }   // phần đệm CÓ bấm được

Cử chỉ gắn vào view mà nó được áp lên. Áp trước phần đệm thì phần đệm được thêm ra ngoài vùng bấm được.

Cảnh báo

Vẫn cái bẫy đó nhưng hỏng khó chịu hơn: một vùng chạm nhỏ hơn 44pt. Gắn cử chỉ thẳng vào một Image nhỏ thì bạn được vùng chạm đúng bằng cái icon. Hãy đệm trước rồi mới gắn cử chỉ — hoặc thêm .contentShape(Rectangle()) để khai báo cả vùng đã đệm là chạm được.

.frame() là cái mọi người hiểu khác đi

frame không phải “đặt kích thước”. Nó chèn vào một view đề nghị một kích thước cho con của nó rồi đặt kích thước mà con đã tự chọn vào bên trong khoảng đó.

Text("Một câu rất dài và chắc chắn không vừa")
    .frame(width: 100)

Text được mời 100 điểm, xuống dòng cho vừa, và cái frame rốt cuộc rộng 100 và cao đúng bằng phần chữ đã xuống dòng cần đến. frame không cắt cụt gì cả — nó đề nghị, còn Text trả lời.

Đây là lý do .frame(maxWidth: .infinity) là cách bạn khiến thứ gì đó lấp đầy bề rộng sẵn có: nó đổi lời đề nghị thành “bao nhiêu bạn có cũng được”, và những view chấp nhận lời đề nghị sẽ nở ra. Nó không làm gì với một Text có ba chữ, vì Text vẫn chọn đúng bề rộng nó cần. Thêm alignment: để nói đứa con nhỏ hơn ngồi ở đâu trong cái frame lớn hơn.

Tự viết modifier

Bất cứ thứ gì bạn viết quá hai lần đều thuộc về một ViewModifier:

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding(16)
            .background(.regularMaterial, in: .rect(cornerRadius: 12))
    }
}

extension View {
    func cardStyle() -> some View { modifier(CardStyle()) }
}

Cái extension mới là phần quan trọng. Viết modifier(CardStyle()) ở mọi chỗ gọi thì đọc rất tệ và làm lộ cái kiểu ra; .cardStyle() đọc như các modifier có sẵn và cho phép bạn đổi phần cài đặt về sau.

Mẹo

Hãy trả về some View từ những hàm này, đừng bao giờ trả về kiểu cụ thể. Kiểu cụ thể của một chuỗi modifier là chi tiết cài đặt nằm sâu ba lớp, và viết nó ra là đóng băng nó vào API của bạn.

Khi một modifier có vẻ chẳng làm gì

Ba nguyên nhân, theo thứ tự đáng kiểm tra:

  1. Nó nằm sai chỗ trong cái tổ. .listRowSeparator phải nằm trên dòng bên trong List, .navigationTitle trên nội dung bên trong NavigationStack, .searchable ở chỗ một vùng điều hướng tìm thấy nó.
  2. Thứ gì đó bên dưới đã ghi đè environment. Cái .font(.title) của bạn trên cái stack đang bị một .font(.body) gần Text hơn đánh bại.
  3. View không có kích thước như bạn nghĩ. Thêm tạm .border(.red) vào. Phần lớn các báo cáo “modifier của tôi hỏng” là một view vốn đã sai kích thước vì một lý do chẳng liên quan gì đến modifier.