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