SwiftUI UI auto refresh problem

import SwiftUI

class MyData: ObservableObject {
    var id: String = ""
    @Published var count: Int = 0

    init(id: String) {
        self.id = id
    }
}

struct ContentView: View {
    @State var list: [MyData] = [MyData(id: "sub a"), MyData(id: "sub b")]
    @State var count: Int = 0

    var body: some View {
        VStack(spacing: 0) {
            Button("click \(count)") {
                count += 1
//                call from here has no problem
//                setAllType(on: count)
            }

            Divider()
            ForEach(list, id: \.id) {
                type in
                Text("\(type.id): \(type.count)")
            }
        }
        .onChange(of: count) {
//          call from here has  problem
            setAllType(on: count)
        }
        .frame(width: 100, height: 100)
    }

    func setAllType(on: Int) {
        for type in list {
            type.count = on
        }
    }
}

Let me briefly explain my code. I have a class called MyData, which has a @Published var count: Int. At the same time, in the view, I have a @State var list: [MyData] array, and a @State var count: Int. When I click the button, I increment count, and call setAllType to set the count for all MyData instances in the list.

Now the problem arises with the setAllType function. If I call it in the Button's click event, everything works as expected. However, if I use onChange to monitor the changes of count, and then call setAllType, there's an issue. The specific problem is that the count displayed by ForEach for MyData is always one less than the count displayed by the Button. They should be equal.

Answered by Vision Pro Engineer in 782749022

Hi @WangZiYuan ,

You can see what's happening if you put some print statements in your code. In the Button action, put print("update count in button"), in the ForEach put let _ = print("in ForEach") and in the onChange put print("onChange"). In your console when you press "click 0" you'll see:

update count in button
in ForEach
in ForEach
onChange

Now it's much easier to see that your list isn't getting updated because the onChange is being called after the ForEach has already been iterated over. A better way of doing this is to use a struct to hold your data and an observable class as a model like this:

import SwiftUI

struct MyData: Identifiable {
    var id = UUID()
    var title: String = ""
    init(id: UUID = UUID(), title: String) {
        self.id = id
        self.title = title
    }
}

class Model: ObservableObject {
    @Published var count: Int = 0
    @Published var list: [MyData] = [MyData(title: "sub a"), MyData(title: "sub b")]
}

struct ContentView: View {
    @StateObject private var model: Model = Model()
    @State var count: Int = 0

    var body: some View {
        VStack(spacing: 0) {
            Button("click \(count)") {
                count += 1
            }

            Divider()
            ForEach(model.list, id: \.id) { item in
        
                Text("\(item.title): \(model.count)")
            }
        }
        .onChange(of: count) { old, new in
            setAllType(on: new)
        }
        .frame(width: 100, height: 100)
    }

    func setAllType(on: Int) {
        for type in model.list {
            model.count = on
        }
    }
}

Now, if you add those print statements back in, you'll see

update count in button
in ForEach
in ForEach
onChange
in ForEach
in ForEach

This is because you're updating count, which is in the model. The list is also in the model, so it too is being updated.

Problem is that list items are not observed, hence, no change in State var when updating.

To see it, just change the state var count and restore in setAllType():

    func setAllType(on: Int) {
        for type in list {
            type.count = on
        }
        count += 1
        count -= 1
    }

As is, it works.

Accepted Answer

Hi @WangZiYuan ,

You can see what's happening if you put some print statements in your code. In the Button action, put print("update count in button"), in the ForEach put let _ = print("in ForEach") and in the onChange put print("onChange"). In your console when you press "click 0" you'll see:

update count in button
in ForEach
in ForEach
onChange

Now it's much easier to see that your list isn't getting updated because the onChange is being called after the ForEach has already been iterated over. A better way of doing this is to use a struct to hold your data and an observable class as a model like this:

import SwiftUI

struct MyData: Identifiable {
    var id = UUID()
    var title: String = ""
    init(id: UUID = UUID(), title: String) {
        self.id = id
        self.title = title
    }
}

class Model: ObservableObject {
    @Published var count: Int = 0
    @Published var list: [MyData] = [MyData(title: "sub a"), MyData(title: "sub b")]
}

struct ContentView: View {
    @StateObject private var model: Model = Model()
    @State var count: Int = 0

    var body: some View {
        VStack(spacing: 0) {
            Button("click \(count)") {
                count += 1
            }

            Divider()
            ForEach(model.list, id: \.id) { item in
        
                Text("\(item.title): \(model.count)")
            }
        }
        .onChange(of: count) { old, new in
            setAllType(on: new)
        }
        .frame(width: 100, height: 100)
    }

    func setAllType(on: Int) {
        for type in model.list {
            model.count = on
        }
    }
}

Now, if you add those print statements back in, you'll see

update count in button
in ForEach
in ForEach
onChange
in ForEach
in ForEach

This is because you're updating count, which is in the model. The list is also in the model, so it too is being updated.

SwiftUI UI auto refresh problem
 
 
Q