Trang chủ

Ai sở hữu giá trị

@State, @Binding, và đọc ngược từ lỗi về chỗ sai

Hãy bắt đầu bằng câu hỏi quyết định tất cả: nếu view này biến mất, giá trị có nên biến mất theo không?

Có — @State. Cái công tắc trong một dòng cài đặt, chuyện một nhóm gấp mở đang mở hay đóng, đoạn chữ đang được gõ vào ô tìm kiếm trước khi bấm xác nhận. Không ai khác cần đến nó và chẳng mất gì khi view ra đi.

Không — nó thuộc về thứ gì đó ở trên, và view nhận một @Binding hoặc đọc một đối tượng mô hình.

@State là chỗ chứa, không phải giá trị

@State không phải “một biến trong struct”. Struct của bạn bị vứt đi liên tục, nên giá trị không thể sống ở đó. @State cấp phát một cái hộp trong bộ nhớ của chính SwiftUI rồi giữ một con trỏ tới nó; struct giữ con trỏ, framework giữ cái hộp, và cái hộp sống sót qua mọi lần vẽ lại chừng nào danh tính của view còn.

Ba hệ quả đi theo, và mỗi hệ quả là một quy tắc người ta học được sau khi bị cắn:

struct Counter: View {
    @State private var count = 0

Luôn để private. Cái hộp thuộc về view này. Một @State mà view khác đặt được là một @Binding viết sai, và truyền một giá trị vào nó từ view cha không làm điều mà nó trông như đang làm.

Giá trị khởi tạo chỉ dùng một lần. Nó khởi tạo cái hộp ở lần đầu danh tính này xuất hiện, và bị lờ đi ở mọi lần cập nhật sau. Đây là nguồn gốc của cái lỗi kinh điển:

struct Badge: View {
    let colour: Colour
    @State private var current: Colour

    init(colour: Colour) {
        self.colour = colour
        _current = State(initialValue: colour)   // đặt một lần, không bao giờ nữa
    }
}

Truyền một colour mới vào và current không đổi, vì cái hộp đã tồn tại rồi. Nếu bạn cần trạng thái đi theo một thuộc tính, thứ bạn muốn là onChange(of:), hoặc .id() để cố ý biến nó thành một view mới — đã nói ở chương về danh tính.

Dùng nó cho kiểu giá trị. @State giữ một thực thể class thì lưu cái tham chiếu, nên sửa một thuộc tính của đối tượng đó chẳng đổi thứ gì SwiftUI thấy được. Nó không bao giờ nhận ra, vì con trỏ nó đem so vẫn y hệt. Đó là lý do @Observable tồn tại.

@Binding là quyền ghi được ủy nhiệm

Một binding không phải bản sao của một giá trị. Nó là một cặp closure — một cái đọc và một cái ghi — trỏ tới chỗ chứa nằm ở nơi khác.

struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        ToggleRow(isOn: $isOn)
    }
}

struct ToggleRow: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Thông báo", isOn: $isOn)
    }
}

$isOn ở view cha chiếu cái @State thành một Binding<Bool>. ToggleRow đọc và ghi nó được mà không sở hữu nó, và không cần biết chỗ chứa thật là @State, một thuộc tính của một mô hình @Observable, hay một binding khác đã truyền xuống ba tầng.

Hai điều đáng biết. Một binding có thể dựng ra từ hư không, bằng .constant(true), đúng thứ mà preview cần. Và binding ghép xuống được tới các giá trị con — $user.name cho bạn một Binding<String> ghi vào trường name của một Binding<User> — thứ dẹp đi phần lớn đoạn nối ống mà người ta hay viết tay.

Cảnh báo

Đừng tạo một Binding(get:set:) ngay trong body để né một lỗi biên dịch. Thỉnh thoảng nó đúng là công cụ cần dùng, nhưng nó chạy closure của bạn ở mọi lần đọc, và một setter có tác dụng phụ bên trong sẽ nổ vào những lúc bạn không định. Nếu bạn thấy mình viết một cái để “cho vừa kiểu” thì quyền sở hữu đang sai ở tầng trên.

Khi cả hai đều không hợp, đó là một đối tượng mô hình

Ngữ nghĩa giá trị thôi giúp ích khi nhiều view chẳng liên quan đến nhau cùng cần thấy một thay đổi — một giỏ hàng, một trình phát, một bộ máy đồng bộ. Đó là một kiểu tham chiếu, đánh dấu @Observable, và đọc thẳng:

@Observable
final class Cart {
    var items: [Item] = []
    var total: Decimal { items.reduce(0) { $0 + $1.price } }
}

struct CartView: View {
    let cart: Cart          // không cần wrapper nào để đọc

    var body: some View {
        List(cart.items) { ItemRow(item: $0) }
    }
}

Để ý là không có property wrapper nào trên cart. Việc một view đọc một @Observable đã đủ để SwiftUI theo dõi nó. Bạn chỉ cần @Bindable khi muốn một binding vào bên trong nó — $cart.name cho một TextField — và cần @State khi chính view phải sở hữu và sống lâu hơn đối tượng đó.

Đọc ngược từ lỗi

Các triệu chứng đủ đặc trưng để chẩn đoán ngược:

Triệu chứng Nguyên nhân thường gặp
View không cập nhật gì cả Sửa một class không phải @Observable, hoặc một thuộc tính let thuần
Trạng thái bị đặt lại bất ngờ Danh tính đổi — thường là một if với hai nhánh giống nhau
Truyền giá trị mới vào chẳng có tác dụng @State khởi tạo từ một thuộc tính; cái hộp đã tồn tại rồi
Mọi thứ vẽ lại ở mọi thay đổi Trạng thái nằm quá cao trong cây, hoặc đọc nguyên cả mô hình
“Modifying state during view update” Ghi trạng thái bên trong body thay vì trong một hành động hay .task