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.likes
to the Equatable conformance (which is not intended) - modify
BookView
to accept@Binding var book: Book
instead 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.
CodePudding user response:
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 Book
s 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 Book
s, 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.