SwiftUI View not updating based on @ObservedObject
Solution 1
With @ObservedObject
everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible
array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text
is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send()
and use instead default @Published
pattern in view model
class NumberLinex: ObservableObject {
@Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach
still not updated... yes, because problem in ForEach
- you used constructor with Range
that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach
, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach
constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
@ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
@Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
Solution 2
I faced the same issue. For me, replacing @ObservedObject with @StateObject worked.
Solution 3
Using your insight, @Asperi, that the problem is with the ForEach
and not with the @ObservableObject
functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
@ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
@Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
Solution 4
There is nothing Wrong with observed object, you should use @Published
in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
@ObservedObject var model = NumberLineModel()
@State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
Related videos on Youtube
Anton
Former mathematician, longtime professional jazz saxophonist & composer, getting back into the tech world through Swift after 20 years away… and loving it!
Updated on April 15, 2022Comments
-
Anton about 2 years
In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual
objectWillChange.send()
call. I would have thought that should trigger the update, but the screen never changes.Any idea? I'd be interested in a solution using
NumberLine
as a class, or as a struct, or using noNumberLine
type at all and instead rather just using an array variable within theContentView
struct.Here's the code:
import SwiftUI struct ContentView: View { @ObservedObject var numberLine = NumberLine() var body: some View { VStack { HStack { ForEach(0 ..< numberLine.visible.count) { number in if self.numberLine.visible[number] { Text(String(number)).font(.title).padding(5) } } }.padding() Button(action: { let index = Int.random(in: 0 ..< self.numberLine.visible.count) self.numberLine.objectWillChange.send() self.numberLine.visible[index].toggle() print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")") }) { Text("Change") }.padding() } } } class NumberLine: ObservableObject { var visible: [Bool] = Array(repeatElement(true, count: 10)) }
-
João Serra about 3 yearsin my case it was a stupid issue, i had to put the observedObject being updated on main thread... suddentlly everythiing start working as expected
-
swiftPunk about 3 yearsI answered your question in my answer!
-
-
Anton about 4 yearsThank you, @asperi! The question remains for me of why the
if number.visible
clause surrounding theText()
views didn't prevent the invisible numbers from showing. Is it that when theForEach
iterates over a constant collection it doesn't even receive the notification that thenumberLine
in its closure has changed? -
Pacu over 3 yearsI was having the same problem on iOS 13 with xcode 11.6, but not on iOS 14 beta. I was using indeed the range approach. then I changed to have an identifiable model, but the problem persisted. Adding @published to the property made the trick for me. iOS 14 beta worked all the time.
-
cgold over 3 yearsWhat is the expected error? When I run the original code by the poster in Xcode 12.1, clicking the button does change the output. So either I'm misunderstanding the error or Swift changed how it handles this case?
-
jeremyabannister over 3 yearsThank you!! I owe a huge debt of thanks to: 1) Anton for taking the time to post this code, 2) @Asperi for knowing the answer and taking the time to write it out, 3) StackOverflow for having created a platform where the two of you could find each other, and 4) Google for miraculously transforming my rather vague query into the exact StackOverflow link that that I needed. It took me all of 2 minutes to debug a thing I was fearing would take hours. Who knew about this behavior of
ForEach
!? 🙏🙏 -
Anton about 3 yearsThank you, swiftPunk, but your code is solving a different problem from mine. Yours works because it replaces every eliminated number with another, resulting in a constant number of items in the ForEach. As @Asperi pointed out above, mine failed because it required a variable number of items on the screen… but the ForEach variety I was using was the one requiring a constant number of items. He explains it well in his solution, and mine merely streamlines his.
-
swiftPunk about 3 yearsNo problem, just wanted help!
-
S. Kaan Özkaya about 3 yearsThis change solving the problem has no connection with the problem here.
-
Mike McCartin almost 3 yearsI found this question facing a similar issue regarding async changes not updating an ObservedObject, and indeed this change solved my problem as well. No idea why, but if anyone comes across this thread be sure to give this a try.
-
Cédric Moreaux over 2 yearsI solved my problem too, would be interested by an the reason if somebody has it 😊
-
Nat Serrano over 2 yearswtf, same for me! it worked, I spent a day debugging this....why Apple???????