I'm new to SwiftUI. I'm sorry if this is a stupid question.
@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)
}
}
The ChangeView
is reading and watching book.pageOne.isAvailable
. Every time I click the Toggle, the ChangeView
's body will be rebuilt. That makes sense.
But I found the DisplayView
's body is also rebuilt every time I click the Toggle even if it doesn't care about the isAvailable
at all. So my question is why SwiftUI rebuild DisplayView
when a variable it doesn't care about changes? It seems to be different from what was said in the document. Is it because the isAvaialble
is a subproperty of Observable data?
First, you should make Page
also @Observable
. Otherwise SwiftUI cannot track its changes properly.
@Observable
class Page {
var pageTitle = "pageTitle"
var isAvailable = true
}
Doing this alone is not enough to fix the problem, though. To fix this, you can either make a Bindable
of the Page
(in which case book
need not be @Bindable
),
@Bindable var page = book.pageOne
Toggle(book.pageOne.isAvailable ? "page available" : "page not available",
isOn: $page.isAvailable
)
or do:
Toggle(book.pageOne.isAvailable ? "page available" : "page not available",
isOn: $book[dynamicMember: \.pageOne.isAvailable]
)
Explanation:
When you do $book.pageOne.isAvailable
, it is lowered (i.e. equivalent) to:
$book[dynamicMember: \.pageOne][dynamicMember: \.isAvailable]
The first pair of []
is a call to Bindable.subscript
. This creates a Binding<Page>
. The second pair of []
is a call to Binding.subscript
, creating a Bindable<Bool>
.
If you have no idea what this is all about, see SE-0195 and SE-0252.
The problem is this intermediate Binding<Page>
. When the final Binding<Bool>
gets changed by the toggle, the setter of isAvailable
is obviously called, but from my observations, the setter of pageOne
is also called.
As Page
is a reference type, this setter call is unnecessary. Perhaps this is because the Binding<Bool>
is produced from the Binding<Page>
. Binding
s are originally designed to work with structs after all, and if Page
were a struct, it is absolutely necessary to call the setter of pageOne
.
In any case, because the setter of pageOne
is called, when isAvailable
changes, SwiftUI thinks that pageOne
also changes. Since DisplayView.body
calls the getter of pageOne
, SwiftUI would call DisplayView.body
again when isAvailable
changes.
In both solutions, I eliminated the intermediate Binding<Page>
, so the setter of pageOne
does not get called.