SwiftUI: почему SwiftUI перестраивает представление при изменении нерелевантного свойства данных @Observable

Я новичок в SwiftUI. Извините, если это глупый вопрос.

@Observable
class Book {
    var title = "A Sample Book"
    var pageOne = Page()
}

class Page {
    var pageTitle = "pageTitle"
    var isAvailable = true
}

struct ContentView: View {
    @State private var book = Book()
    
    var body: some View {
        VStack{
            ChangeView(book: book)
            DisplayView(book: book)
        }
    }
}

struct ChangeView: View{
    @Bindable var book: Book
    
    var body: some View{
        Toggle(book.pageOne.isAvailable ? "page available" : "page not available",
               isOn: $book.pageOne.isAvailable
        )
    }
}

struct DisplayView: View {
    var book: Book

    var body: some View {
        Text(book.pageOne.pageTitle)
    }
}

ChangeView читает и смотрит book.pageOne.isAvailable. Каждый раз, когда я нажимаю переключатель, тело ChangeView перестраивается. Это имеет смысл. Но я обнаружил, что тело DisplayView также перестраивается каждый раз, когда я нажимаю переключатель, даже если оно вообще не заботится о isAvailable. Итак, мой вопрос: зачем SwiftUI перестраивать DisplayView, если переменная не имеет значения? Кажется, это отличается от того, что сказано в документе. Это потому, что isAvaialble является подсвойством данных Observable?


1
52
1

Ответ:

Решено

Во-первых, вам также следует сделать Page@Observable. В противном случае SwiftUI не сможет должным образом отслеживать свои изменения.

@Observable
class Page {
    var pageTitle = "pageTitle"
    var isAvailable = true
}

Однако одного этого недостаточно, чтобы решить проблему. Чтобы это исправить, вы можете либо поставить Bindable вместо Page (в этом случае book не обязательно должно быть @Bindable),

@Bindable var page = book.pageOne
Toggle(book.pageOne.isAvailable ? "page available" : "page not available",
       isOn: $page.isAvailable
)

или сделайте:

Toggle(book.pageOne.isAvailable ? "page available" : "page not available",
       isOn: $book[dynamicMember: \.pageOne.isAvailable]
)

Объяснение:

Когда вы это делаете $book.pageOne.isAvailable, он снижается (т. е. эквивалентно) до:

$book[dynamicMember: \.pageOne][dynamicMember: \.isAvailable]

Первая пара [] — это вызов Bindable.subscript . Это создает Binding<Page>. Вторая пара [] — это вызов Binding.subscript, создающий Bindable<Bool>.

Если вы понятия не имеете, о чем идет речь, посмотрите SE-0195 и SE-0252.

Проблема в этом промежуточном Binding<Page>. Когда последний Binding<Bool> изменяется переключателем, очевидно, вызывается установщик isAvailable, но, по моим наблюдениям, также вызывается установщик pageOne.

Поскольку Page является ссылочным типом, этот вызов сеттера не нужен. Возможно, это потому, что Binding<Bool> производится из Binding<Page>. В конце концов, Binding изначально предназначены для работы со структурами, и если бы Page была структурой, абсолютно необходимо вызвать установщик pageOne.

В любом случае, поскольку вызывается установщик pageOne, когда isAvailable изменяется, SwiftUI думает, что pageOne тоже меняется. Поскольку DisplayView.body вызывает метод получения pageOne, SwiftUI будет вызывать DisplayView.body снова, когда isAvailable изменится.

В обоих решениях я исключил промежуточный Binding<Page>, поэтому установщик pageOne не вызывается.