0

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))
                        })
                    }
                }
            }
        }
    }
}
Peanutsmasher
  • 220
  • 3
  • 13
  • You can’t do that. For the simple reason that an array is not an ObservableObject. Make a CDUserButtonView so you can observe the item in your loop – lorem ipsum Aug 13 '21 at 21:09
  • 1
    https://stackoverflow.com/questions/68710726/swiftui-view-updating/68713038#68713038 – lorem ipsum Aug 13 '21 at 21:12
  • Fully agree that creating a child view with the ObservedObject works for the initial example. I have added a more complex example to the question, in which the child view strategy does not work, as the list is based on the FetchRequest not an ObservedObject. How would you solve this, as I can't pass the FetchedResult array to an ObservedObject of a child view? @loremipsum – Peanutsmasher Aug 13 '21 at 22:30
  • 1
    What part doesn’t update? The item moving from one list to the other? That is because you should be using a nspredicate vs a filter that doesn’t have any built in observers, that is expected behavior the fetch results 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 – lorem ipsum Aug 13 '21 at 23:33
  • @loremipsum - thank you for the clarification. Fetching the data for the two sections via two separate FetchRequests with the according NSPredicate did the trick. It doesn't animate the move as nicely, as when using an array, but that's a different topic. If you post your statement as an answer, I'll gladly accept it – Peanutsmasher Aug 14 '21 at 11:33
  • 1
    @Peanutsmasher Honestly it looks way neater separated as it already is. You should try split views into the smallest possible, rather than keeping every together. I don't think anything should change. Complicated SwiftUI views turn into the [pyramid of doom](https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)). – George Aug 14 '21 at 12:12
  • I'm glad I helped. I added the answer – lorem ipsum Aug 14 '21 at 13:12

1 Answers1

1

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

lorem ipsum
  • 21,175
  • 5
  • 24
  • 48