core-dataswiftuinsfetchrequestobservedobject

SwiftUI - Using an @ObservedObject for the result of an @FetchRequest in the same struct to force-update the UI


To my knowledge, I pass a Core Data Entity object to an @ObservedObject in a child-struct to force the UI to update when I change the Core Data object.

Example:

struct ParentView: View {

@FetchRequest(entity: CDUser.entity(), sortDescriptors: [])
var users: FetchedResults<CDUser>

   var body: some View {
      List {
         ForEach(users, id: \.self) { item in 
            ChildView(item: item)
         }
      }
   }
}


struct ChildView: View {

@Environment(\.managedObjectContext) private var moc
@ObservedObject var item: CDUser

   var body: some View {
      HStack {
         Button(action: {
               item.count = item.count + 1
               do {
                  try moc.save()
               } catch {
                  print(error)
               }
            }, label: {
               Text(String(item.count))
            }
         )
      }
   }
}

The @ObservedObject will force the UI to show the increased item.count when the button is pressed.

While this works great, is there a way to not use the parent-child hierarchy to force the UI update by having both the @FetchRequest and @ObservedObject in the same struct?
Ideally, I'd only have one struct, which contains the button.

Below example will update the Code Data object, but the change will only be reflected in the UI when the View reappears.

Example:

struct MainView: View {

@Environment(\.managedObjectContext) private var moc

@FetchRequest(entity: CDUser.entity(), sortDescriptors: [])
var users: FetchedResults<CDUser>

// HOW DO I INTEGRATE THIS OBSERVED OBJECT TO REFLECT THE BUTTON ACTION IMMEDIATELY IN THE UI?
// @ObservedObject var users: [CDUser]
//

   var body: some View {
      List {
         ForEach(users, id: \.self) { item in 
            Button(action: {
                  item.count = item.count + 1
                  do {
                     try moc.save()
                  } catch {
                     print(error)
                  }
               }, label: {
                  Text(String(item.count))
               }
            )
         }
      }
   }
}

Edit: Adding a more complex example, in which changing the Buttons to Child-Views does not cause the sections to update:

struct MainView: View {

    @Environment(\.managedObjectContext) private var moc

    @FetchRequest(entity: CDUser.entity(), sortDescriptors: [])
    var users: FetchedResults<CDUser>
    
    var body: some View {
        List {
            //Section of logged-in users
            Section(header: Text("Logged in")) {
                ForEach(users.filter({ $0.logged == true }), id: \.self) { user in
                    Button(action: {
                        user.logged = false
                        do {
                            try moc.save()
                        } catch {
                            print(error)
                        }
                    }, label: {
                        Text(String(user.firstName))
                    })
                }
                //Section of logged-out users
                Section(header: Text("Logged out")) {
                    ForEach(users.filter({ $0.logged == false }), id: \.self) { user in
                        Button(action: {
                            user.logged = true
                            do {
                                try moc.save()
                            } catch {
                                print(error)
                            }
                        }, label: {
                            Text(String(user.firstName))
                        })
                    }
                }
            }
        }
    }
}

Solution

  • You can’t do that. Because an array is not an ObservableObject. Make a CDUserButtonView so you can observe the item in your loop

    Your added sections don't update because you should be using a NSPredicate vs a filter that doesn’t have any built in observers, that is expected behavior, the FetchResults only cause a redraw when the array as a whole changes. An array can’t be an observed object you can only observe the elements of the array individually.

    Also, if you are targeting iOS 15+ you should look into SectionedFetchRequest it will create the sections for you.

    https://developer.apple.com/wwdc21/10017

    it is at about minute 23:26