I have the following code:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@State var selectedItemMOID:NSManagedObjectID?
var body: some View {
NavigationView {
VStack {
if let selectedItemMOID = selectedItemMOID, let item = viewContext.object(with: selectedItemMOID) as? Item {
ItemView(item: item)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
if let item = addItem() {
selectedItemMOID = item.objectID
}
} label: {
Image(systemName: "plus")
.font(.system(size: 14, weight: .bold))
}
}
}
}
}
private func addItem() -> Item? {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
return newItem
} catch {
return nil
}
}
}
}
struct ItemView: View {
@ObservedObject var item:Item
@State var itemIDString:String = ""
var body: some View {
List {
VStack {
Text(itemIDString)
Text("the item: \(item.objectID)")
}
.onChange(of: item, perform: { newValue in
updateIdString()
})
.onAppear {
updateIdString()
}
}
}
func updateIdString() {
itemIDString = "\(item.objectID)"
}
}
Problem:
when I press the plus button (top right), ItemView
partially updates. The bottom Text view updates immediately, the top one lags behind.
See this behavior:
You can see the item id at top is always one behind (except first render). So, for example, hit plus (after first time) and top is p45, bottom, p46. And so on.
Why is the top Text lagging one behind?
I added the onChange
as well, thinking that would sync things up, but it didn't. Without it, the top Text never updates after first item is set.
.onChange(of: item, perform: { newValue in
updateIdString()
})
I need to understand why this is happening so I can understand how to properly build views in general that use @ObservedObject
s, so things update immediately when the ObservedObject is set to a new one.
So, to be clear my question is how to fix this code so that the entire ItemView
updates immediately when item
is set on it and why this is happening at a high level so I can avoid this same type of mistake going forward.
To address Asperi's approach (thanks for your answer!):
His answers fixes this specific case, but, I am not sure how I would go about using that pattern. For example, if ItemView
observes multiple objects, lets say a Car
and a User
entity, then .id(car.id)
would only react to setting a new Car
on ItemView
and not react to setting a new User
necessarely. So, then, what is the point of @ObservedObject
in this context. With the suggested .id(car.id)
it would only react to one and not the other (or many more if ItemView
had like 6 @ObservedObjects...).
Does this mean a view should only have one @ObservedObject? I wouldn't think so. Am I wrong?
Just trying to understand how to scale the id(item.id)
pattern if it's the agreeable answer. I don't understand yet.
CodePudding user response:
I think it is due to reuse/caching, in short rendering optimisation, try
List {
// content is here
}
.id(item) // << this one !!
CodePudding user response:
It's because you didn't use the newValue, change it to:
.onChange(of: item, perform: { newValue in
itemIDString = "\(newValue.objectID)"
})