Replacing array in Observed object won't update UI.

Hello, following is the issue: I have a @Observable view model which has an array of @Observable Items.

Tapping an item leads to a detail like view to submit a value to the item. This work thru bindings. However I have the need to replace the contents of the array entirely with a fresh version loaded from the network. It will contain the "same" objects with the same id but some values might have changed. So replacing the entire array seems to not update the UI because the IDs are the same as before. Also this seems to break the bindings because when replacing the array, editing no longer updates the UI.

How to test the behavior:

  1. Launch the app in simulator.
  2. Add some values to the items by tapping on an item and then on add.
  3. Notice how changes are updated.
  4. Tap the blue button to sync fresh data to the array. (Not replacing the actual array)
  5. Confirm everything is still working
  6. Replace the array with the red button.
  7. Editing and UI updates are broken from now on.

What is the proper way to handle this scenario?

Project: https://github.com/ChristianSchuster/DTS_DataReplaceExample.git

Answered by DTS Engineer in 816921022

I believe the reason SwiftUI doesn't update the list is because the items have no change based on your implementation. Look at your following code:

@Observable
final class MyObservableItem: Codable, Identifiable, Hashable, Equatable  {
...
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: MyObservableItem, rhs: MyObservableItem) -> Bool {
        return lhs.id == rhs.id
    }

This Hashable implementation tells Swift that items with the same id are the same. With this, when you render items with the following code:

ForEach($model.items, id: \.self) { $item in

SwiftUI checks the hashes of the items (because you specify .self for the id parameter) and finds no change (because item.id isn't changed), and hence determines that no update is needed.

To address this issue, you may re-consider the equality of the items. For example, if items that have the same id but different currentValue are not the same, you can implement Hashable in the following way:

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

With this, SwiftUI should update the list when currentValue changes.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I believe the reason SwiftUI doesn't update the list is because the items have no change based on your implementation. Look at your following code:

@Observable
final class MyObservableItem: Codable, Identifiable, Hashable, Equatable  {
...
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: MyObservableItem, rhs: MyObservableItem) -> Bool {
        return lhs.id == rhs.id
    }

This Hashable implementation tells Swift that items with the same id are the same. With this, when you render items with the following code:

ForEach($model.items, id: \.self) { $item in

SwiftUI checks the hashes of the items (because you specify .self for the id parameter) and finds no change (because item.id isn't changed), and hence determines that no update is needed.

To address this issue, you may re-consider the equality of the items. For example, if items that have the same id but different currentValue are not the same, you can implement Hashable in the following way:

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

With this, SwiftUI should update the list when currentValue changes.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for going over it. Your explanations makes sense and I understand what you say but I think there seems to be a problem when modifying the "ID" of on object.

Im not sure if there is a distinction between the "ID" and "hasChanged". Also this leads to runtime crashes with following error:

Thread 1: Fatal error: Duplicate keys of type 'MyObservableItem' were found in a Dictionary.

I think the correct way to handle this should be to leave the ID the same (also for things like reordering animations) and somehow still indicating a change.

Including a changing property in the hash or == means that changing a property also changes the identity of an object, at least when using .self as the ID, which seems to create issues on how SwiftUI seems to track changes to an ForEach loop. One MyObservableItem seems to be in a Dictionary associated with a key (probably the id). Manipulating the hash with property changes results in unexpected behavior and crashes.

Replacing array in Observed object won't update UI.
 
 
Q