Reputation: 3257
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))
}
Upvotes: 49
Views: 65232
Reputation: 1076
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.
Upvotes: 1
Reputation: 14321
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
}
Upvotes: 8
Reputation: 1
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!
}
Upvotes: 0
Reputation: 520
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.
Upvotes: 2
Reputation: 257493
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
}
Upvotes: 40
Reputation: 437
The problem is with the function, do not forget to add id: \.self
in your ForEach function, and make your Model Hashable, Identifiable
.
Upvotes: 2
Reputation: 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.
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))
}
Upvotes: 1
Reputation: 559
I faced the same issue. For me, replacing @ObservedObject with @StateObject worked.
Upvotes: 32
Reputation: 3257
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))
}
Upvotes: 5