Home > database >  SwiftUI does not reliably propagate changes to child view
SwiftUI does not reliably propagate changes to child view

Time:04-12

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.

enter image description here

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 a let

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 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.

  • Related