Search code examples
iosswiftlistviewswiftuirefresh

SwifUI ForEach List keeps modified values when reloading a @Published array inside ObservableObject


I have a SwiftUI List populated using a ForEach loop binded to a @Published array inside an ObservableObject

Elements in the list can be modified with an on/off flag. When the list is cleared out all items seems removed from the UI but when the data is refetched and loaded inside the @Published array the UI still shows the state of te previously modified elements. As you can see in the following video:

List not refreshing demo

The code to reproduce it:

struct ListView: View {
    
    @ObservedObject private var viewModel = ListViewModel()

    var body: some View {
            VStack {
                List {
                    ForEach(viewModel.list, id: \.self) { element in
                        ElementRow(element: element)
                    }
                }
                HStack {
                    Button(action: { viewModel.fetch() }) { Text("Refresh") }
                    Button(action: { viewModel.remove() }) { Text("Remove") }
                }
            }.onAppear {
                viewModel.fetch()
            }
        }
    }

struct ElementRow: View {
    @ObservedObject var element: ElementViewModel
    
    var body: some View {
        HStack {
            Text(element.id)
            Spacer()
            Toggle("", isOn: $element.on )
        }
    }
}

class ListViewModel : ObservableObject {
    
    @Published var list: [ElementViewModel] = []
    
    func fetch() {
        list.append(contentsOf: [ElementViewModel(id: "0", on: false), ElementViewModel(id: "1", on: false)])
    }
    func remove() {
        list.removeAll()
    }
}

class ElementViewModel : ObservableObject, Hashable {
    
    var id: String = ""
    @Published var on: Bool = false
    
    init(id: String, on: Bool) {
        self.id = id
        self.on = on
    }
    static func == (lhs: ElementViewModel, rhs: ElementViewModel) -> Bool {
        lhs.id == rhs.id
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

What needs to be changed to make that all the elements in the list remains off when it's refreshed?


Solution

  • Your model objects is interpreted as equal, so views are not updated (List caches rows similarly to UITableView).

    Find below a fix. Tested with Xcode 12.4 / iOS 14.4

    class ElementViewModel : ObservableObject, Hashable {
    
        // ... other code
    
        static func == (lhs: ElementViewModel, rhs: ElementViewModel) -> Bool {
            lhs.id == rhs.id && lhs.on == rhs.on    // << here !!
        }
    
        // ... other code
    }