SwiftUI View not updating based on @ObservedObject

30,317

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

demo

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)

demo2

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
}

backup

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.


enter image description here


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))
    
}

Share:
30,317

Related videos on Youtube

Anton
Author by

Anton

Former mathematician, longtime professional jazz saxophonist &amp; composer, getting back into the tech world through Swift after 20 years away… and loving it!

Updated on April 15, 2022

Comments

  • Anton
    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 no NumberLine type at all and instead rather just using an array variable within the ContentView struct.

    Screenshot

    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
      João Serra about 3 years
      in 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
      swiftPunk about 3 years
      I answered your question in my answer!
  • Anton
    Anton about 4 years
    Thank you, @asperi! The question remains for me of why the if number.visible clause surrounding the Text() views didn't prevent the invisible numbers from showing. Is it that when the ForEach iterates over a constant collection it doesn't even receive the notification that the numberLine in its closure has changed?
  • Pacu
    Pacu over 3 years
    I 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
    cgold over 3 years
    What 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
    jeremyabannister over 3 years
    Thank 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
    Anton about 3 years
    Thank 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
    swiftPunk about 3 years
    No problem, just wanted help!
  • S. Kaan Özkaya
    S. Kaan Özkaya about 3 years
    This change solving the problem has no connection with the problem here.
  • Mike McCartin
    Mike McCartin almost 3 years
    I 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
    Cédric Moreaux over 2 years
    I solved my problem too, would be interested by an the reason if somebody has it 😊
  • Nat Serrano
    Nat Serrano over 2 years
    wtf, same for me! it worked, I spent a day debugging this....why Apple???????