SwiftUI view not being refreshed when Published object changes

703 Views Asked by At

In my view model I have this property and method:

@Published var cats: [Cat] = [] //this gets populated later

When I update one of the cats as such:

func updateCatQuantity(_ cat:Cat, qty: Int) {
    if let index = cats(of: cat) {
        cats[index].quantity = qty
    }
}

The view does not get refreshed. didSet on cats does not get called. I think it has something to do with the fact that Cat is a class instead of a struct. Here's how it's defined:

class Cat: BaseMapperModel, Identifiable, Hashable {
    static func == (lhs: Cat, rhs: Cat) -> Bool {
       return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }

    var id = UUID()

    var title: String = ""
    var quantity: Int = 0

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    override func mapping(map: Map) {
        title <- map["title"]
        quantity <- map["qty"]
    }
}

How can I get the view to refresh when the quantity of a cat is changed?

1

There are 1 best solutions below

11
lorem ipsum On

There are 2 issues

  1. @Published only triggers a refresh when the "value" changes. Changing a variable in a class is not considered a "value" change.

Switching to a struct is the "easy" change.

struct Cat: Identifiable, Hashable {
    var id = UUID()

    var title: String = ""
    var quantity: Int = 0
}

But issue #2 is a bigger deal. By overriding Hashable you are telling SwiftUI to only trigger View reloads when the id changes.

To observe a class in iOS 13-16 the object has to conform to ObservableObject and be wrapped with @StateObject, @ObservedObject or @EnvironmentObject, appropriately at every level you want to see changes.

class Cat: Identifiable, Hashable, ObservableObject {
    var id = UUID()

    @Published var title: String = ""
    @Published var quantity: Int = 0

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(title)
        hasher.combine(quantity)
    }
    static func == (lhs: Cat, rhs: Cat) -> Bool {
        return lhs.id == rhs.id 
        && lhs.title == rhs.title
        && lhs.quantity == rhs.quantity
    }
}

in iOS 17+ you can use @Observable instead of an ObservableObject with @State and @Bindable appropriately.

@Observable
class Cat: Identifiable{
    var id = UUID()

    var title: String = ""
    var quantity: Int = 0
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(title)
        hasher.combine(quantity)
    }
    
    static func == (lhs: Cat, rhs: Cat) -> Bool {
        return lhs.id == rhs.id
        && lhs.title == rhs.title
        && lhs.quantity == rhs.quantity
    }
}