50

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))
}
1
  • 1
    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 Commented Mar 25, 2021 at 16:13

9 Answers 9

40

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
}
Sign up to request clarification or add additional context in comments.

4 Comments

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?
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.
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?
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 !? 🙏🙏
34

I faced the same issue. For me, replacing @ObservedObject with @StateObject worked.

3 Comments

This change solving the problem has no connection with the problem here.
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.
An instance of ObservableObject wrapped in @StateObject is tied to the view's lifecycle, while one in @ObservedObject is not. I don't know why it solves anything in this example, though.
9

This answer might not be useful until XCode15/iOS17 is released but I'm using the beta right now.

I'm showing a list of MyItem in a Table and I just want the row to update when the status changes but no matter what I can't get this to happen without some .id() hack on the grid.

I tried conforming to hashable and identifiable and setting .self as the id for the rows, along with a bunch of other things, without getting this to work.

Then I simply tried using the new Observable macro and it works flawlessly.

Original Code:

class MyParentObject: ObservableObject {
    @Published var items = [MyItem]()
}

class MyItem: Identifiable, ObservableObject {
    let id = UUID()
    @Published var status: MyStatusEnum = .ready
}

Working Code:

class MyParentObject: ObservableObject {
    @Published var items = [MyItem]()
}

@Observable class MyItem: Identifiable {
    let id = UUID()
    var status: MyStatusEnum = .ready
}

1 Comment

This worked for me. This is particularly useful for tracking optionals and collections of objects.
5

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

1 Comment

Exactly, the issue is not with the '@ObservableObject', need to wrap datasource in '@ObservableObject'
2

The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

Comments

2

I was having the same problem and after hours of trial and error it finally worked. My problem was that I setup my views with ForEach in SwiftUI and only referenced the id: \.id. By doing so, views did not update when a @Published var within my @ObservedObject got updated.

The solution is as Amir mentioned: An @ObservedObject needs to conform to Hashable, Identifiable and the ForEach must not use the id to be identified, but instead either explicitly use id: \.self or it also can be omitted in SwiftUI.

Comments

1

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

1 Comment

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

In my case, I was creating multiple instance of ObservableObject class. I had to make it singleton so that the state is shared between views.

Comments

0

In my case I came here because I have a Model of a card game, and it is encapsulated inside a View Model. But updates were not happening in the View as the model changed, as expected.

My Model is a class (could be a struct) called X, and the ViewModel is a class called XGame which encapsulates the Model as a private var.

// The View Model
class XGame : ObservableObject {
  private var model = X() // OOPS! This was my mistake here
}

// The View
struct ContentView : View {
  @ObservedObject var game: XGame = XGame()
//etc.
}

So once I fixed the View Model and put @Published in front of the private var model (as per below) then everything started working as expected.

// The View Model
class XGame : ObservableObject {
  @Published private var model = X() // Now it works!
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.