Trang chủ

Observation

SwiftUI biết một view thật sự đọc những thuộc tính nào bằng cách nào

@Observable trông như một cách viết ngắn hơn của ObservableObject. Nó là một cơ chế khác, và chính sự khác biệt ấy mới là điều đáng nói.

@Observable
final class Library {
    var books: [Book] = []
    var searchText = ""
    var isSyncing = false
}

Không @Published, không ObservableObject, không objectWillChange. Macro viết lại mọi thuộc tính lưu trữ thành thuộc tính tính toán, thứ báo cáo mọi lần đọc và ghi cho một ObservationRegistrar.

Mô hình cũ thông báo; mô hình mới được hỏi

Với ObservableObject, đối tượng bắn objectWillChange ở mọi lần ghi vào một @Published, và mọi view đang quan sát nó đều vẽ lại — bất kể nó có động đến thuộc tính vừa đổi hay không. Một view chỉ hiện books vẫn vẽ lại mỗi lần isSyncing lật hoặc mỗi lần gõ một ký tự vào searchText.

Với @Observable, SwiftUI tính body bên trong một phạm vi theo dõi. Mọi thuộc tính được đọc trong lần tính đó đều được ghi nhận, và view chỉ bị làm mất hiệu lực khi một trong những thuộc tính ấy thay đổi.

struct BookList: View {
    let library: Library

    var body: some View {
        List(library.books) { BookRow(book: $0) }   // chỉ đọc `books`
    }
}

Gõ vào ô tìm kiếm không còn vẽ lại view này nữa. Không có gì phải cấu hình để đạt được điều đó — nó tự rơi ra từ việc body tình cờ đọc những thuộc tính nào.

Mẹo

Việc theo dõi diễn ra theo từng lần tính, không phải theo kiểu view. Một view chỉ đọc một thuộc tính bên trong một nhánh if thì không theo dõi nó khi nhánh ấy sai. Vào nhánh đó, và phụ thuộc xuất hiện ở lần cập nhật kế tiếp.

Viết gì, và thôi viết gì

Wrapper đặt trên thuộc tính được quyết định bởi việc view làm gì với đối tượng, chứ không bởi bản thân đối tượng:

struct LibraryScreen: View {
    @State private var library = Library()      // view này sở hữu nó

    var body: some View {
        BookList(library: library)              // cứ truyền đi; đọc thì không cần wrapper
        SearchField(library: library)
    }
}

struct SearchField: View {
    @Bindable var library: Library              // cần một binding vào bên trong

    var body: some View {
        TextField("Tìm kiếm", text: $library.searchText)
    }
}
  • Không gì cả — đọc thôi là đủ, và đây là trường hợp phổ biến.
  • @State — view này tạo ra đối tượng và nó nên sống đúng bằng đời của view.
  • @Bindable — bạn cần $model.property cho một TextField, Toggle hay tương tự.
  • @Environment(Library.self) — nó được tiêm từ trên xuống.

@ObservedObject, @StateObject, @EnvironmentObject và @Published là thế hệ trước. Bạn sẽ gặp chúng trong code có sẵn và không cần vội chuyển đổi, nhưng không nên có thứ gì mới dùng đến chúng.

Chỗ việc theo dõi hỏng lặng lẽ

Observation chỉ thấy được thứ nó chặn được, và có ba khoảng hở thật sự.

Thuộc tính tính toán dựa trên chỗ chứa không được quan sát. Một thuộc tính tính toán đọc một private var thuần mà macro không viết lại thì vô hình; chẳng có gì báo cáo lần đọc đó.

Đọc bên ngoài body. Việc theo dõi được thiết lập trong lúc body đang được tính. Một thuộc tính đọc bên trong closure của Task, trong completion handler hay trong onAppear không phải là một phụ thuộc, và sửa nó về sau sẽ chẳng vẽ lại gì cả.

Tập hợp các đối tượng quan sát được. library.books theo dõi cái mảng — chèn vào, xóa đi, đảo thứ tự. Nó không theo dõi một thuộc tính bên trong từng Book. Nếu Book là một class, hãy đánh dấu nó @Observable luôn rồi để mỗi dòng tự đọc nó, và đó cũng là điều bạn muốn: chỉ đúng dòng đó vẽ lại.

struct BookRow: View {
    let book: Book              // class @Observable

    var body: some View {
        Text(book.title)        // chỉ dòng này vẽ lại khi tiêu đề đổi
    }
}

Cái cắn đau nhất

Truyền cả một mô hình xuống rồi đọc nó ở trên cùng:

struct Dashboard: View {
    let library: Library

    var body: some View {
        let count = library.books.count      // một lần đọc, ngay tại Dashboard
        Header(count: count)
        BookList(library: library)
    }
}

Dashboard giờ phụ thuộc vào books, nên nó được tính lại ở mọi thay đổi của mảng — và cả cây con của nó bị dựng lại theo. Hãy đẩy lần đọc ấy xuống view thật sự cần nó, rồi Dashboard sẽ thôi bận tâm.

Đó vẫn là quy tắc “trạng thái nằm ở view thấp nhất cần đến nó” từ phần mở đầu, ở dạng nó thường xuất hiện trong code thật: không phải trạng thái được lưu ở đâu, mà là nó được đọc ở đâu.