'SwiftUI does not reliably propagate changes to child view
I am observing a peculiar behavior in my SwiftUI code and narrowed it down to the following minimal example.
Given this example storage holding an array of book model structs.
struct Book: Identifiable {
let id: UUID
var likes: Int
var unusedProperty: String = ""
}
extension Book: Equatable {
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.id == rhs.id
}
}
class MyStorage: ObservableObject {
@Published var books: [Book] = [
.init(id: .init(uuidString: "B2A44450-BC03-47E6-85BE-E89EA69AF5AD")!, likes: 0),
.init(id: .init(uuidString: "F5AB9D18-DF73-433E-BB48-1C757CB6F8A7")!, likes: 0)
]
func addLike(to book: Book) {
for i in books.indices where books[i].id == book.id {
books[i].likes += 1
}
}
}
And using it in this simple view hierarchy:
struct ReducedContentView: View {
@StateObject var storage: MyStorage = MyStorage()
var body: some View {
VStack(spacing: 8) {
ForEach(storage.books) { book in
HStack {
VStack(alignment: .leading) {
Text("Top-Level: \(book.likes)")
BookView(book: book)
}
Spacer()
Button("Add Like") {
storage.addLike(to: book)
}
}.padding(.horizontal)
}
}
}
}
struct BookView: View {
let book: Book
var body: some View {
Text("Nested: \(book.likes)")
.foregroundColor(.red)
}
}
Any changes to the likes property don't propagate to the BookView, only to the "top-level" Text.
Now, if I change one of the following, it works:
- remove the
unusedProperty(which is needed in production) - add
&& lhs.likes == rhs.likesto the Equatable conformance (which is not intended) - modify
BookViewto accept@Binding var book: Bookinstead of alet
The last option is something I could adopt in my production code - nevertheless, I would really like to understand what's happening here, so any hints would be greatly appreciated.
Solution 1:[1]
This is a result of your custom Equatable conformance:
extension Book: Equatable {
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.id == rhs.id
}
}
If you remove this, it'll work as expected.
In your current code, you're saying that if two Books have the same ID, they are equal. My suspicion is that you don't actually mean that they are truly equal -- you just mean that they are the same book.
Your second option ("add && lhs.likes == rhs.likes to the Equatable conformance (which is not intended)") essentially just uses the synthesized Equatable conformance that the system generates, since unusedProperty isn't used -- so, if you were to use the second option, you may as well just remove the custom == function altogether.
I think the decision to make here is whether you really want to tell the system an untruth (that two Books, no matter what their other properties, are equal if they share the same id) or if you should let the system do it's own work telling if the items are equal or not.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jnpdx |

