Home > front end >  SwiftUI: View does not fully update, lags behind when its @ObservedObject is updated from parent?
SwiftUI: View does not fully update, lags behind when its @ObservedObject is updated from parent?

Time:05-08

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:

enter image description here

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 @ObservedObjects, 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)"
})
  • Related