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 no NumberLine
type at all and instead rather just using an array variable within the ContentView
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)) }
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 }
I faced the same issue. For me, replacing @ObservedObject with @StateObject worked.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With