UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Core Data relationship not updating

Forums > Swift

I am trying to change the background color based on the count of a property in an entity with a to many relationship. However, the view needs to refresh twice for a change to be seen. As a workaround, I’m doing a fetch in every view. But there has to be a better way.

Entities: Checklist<->>TestGroup<->>ActionItem<->>TestPoint

When I change testPoint.status from “Pass” to “Fail”, the background color of the TestGroup should change to red. I've cut some of the SwifUI code to get to just the meat so there may be a { or } missing...

TestGroupList

struct FilteredList: View {
  @Environment(\.managedObjectContext) var moc

  @ObservedObject var checklist: Checklist

  var fetchRequest: FetchRequest<TestGroup>

  init(checklist: Checklist, filter: Set<String>) {
    self.checklist = checklist
    fetchRequest = FetchRequest<TestGroup>(entity: TestGroup.entity(),
                                           sortDescriptors: [NSSortDescriptor(keyPath: \TestGroup.order, ascending: true)],
                                           predicate: NSPredicate(format: "belongsToChecklist == %@ && phaseOfFlight IN %@", checklist, filter))
  }

  var testGroups: FetchedResults<TestGroup> {fetchRequest.wrappedValue}

  var body: some View {
    VStack{
      if !testGroups.isEmpty {
        ScrollView {
          VStack (spacing: 20){
            ForEach(testGroups, id:\.self) { testGroup in
              TestGroupListCellView(testGroup: testGroup)

}

TestGroupListCellView

@Environment(\.managedObjectContext) var moc
  @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

  @ObservedObject var testGroup: TestGroup

  var fetchPass: FetchRequest<TestPoint>
   var fetchFail: FetchRequest<TestPoint>
   var fetchNotTested: FetchRequest<TestPoint>

  init(testGroup: TestGroup) {
     self.testGroup = testGroup

     self.fetchPass = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem.belongsToTestGroup == %@ && passOrFail == %@", testGroup, "Pass"))
     self.fetchFail = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem.belongsToTestGroup == %@ && passOrFail == %@", testGroup, "Fail"))
     self.fetchNotTested = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem.belongsToTestGroup == %@ && passOrFail == %@", testGroup, "Not Tested"))
   }

   var fetchedPass: FetchedResults<TestPoint> {fetchPass.wrappedValue}
   var fetchedFail: FetchedResults<TestPoint> {fetchFail.wrappedValue}
   var fetchedNotTested: FetchedResults<TestPoint> {fetchNotTested.wrappedValue}

  var body: some View {
        VStack {
          HStack {
            Text(testGroup.wrappedName)
            Spacer()
            Image(systemName: "pencil.and.ellipsis.rectangle").onTapGesture {
              self.showingActionItems.toggle()
            }.sheet(isPresented: $showingActionItems) {
                ActionItemsListView(testGroup: self.testGroup).environment(\.managedObjectContext, self.moc)
              }
          }
}

ActionItemsListView

@Environment(\.managedObjectContext) var moc
  @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

  @ObservedObject var testGroup: TestGroup

  var body: some View {
    NavigationView{
      VStack {
      List {
        ForEach(testGroup.actionItemsArray, id: \.self) {actionItem in
          ActionItemsListCellView(actionItem: actionItem)
        }
      }
      Button("Close") {
        self.presentationMode.wrappedValue.dismiss()

}

ActionItemsListCellView

@Environment(\.managedObjectContext) var moc

  @ObservedObject var actionItem: ActionItem

  @State var totalStatus = ""
  @State var setColor = (Color.green)

  var fetchPass: FetchRequest<TestPoint>
  var fetchFail: FetchRequest<TestPoint>
  var fetchNotTested: FetchRequest<TestPoint>

  init(actionItem: ActionItem) {
    self.actionItem = actionItem
    self.fetchPass = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem == %@ && passOrFail == %@", actionItem, "Pass"))
    self.fetchFail = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem == %@ && passOrFail == %@", actionItem, "Fail"))
    self.fetchNotTested = FetchRequest(entity: TestPoint.entity(), sortDescriptors: [], predicate: NSPredicate(format: "belongsToActionItem == %@ && passOrFail == %@", actionItem, "Not Tested"))
  }

  var fetchedPass: FetchedResults<TestPoint> {fetchPass.wrappedValue}
  var fetchedFail: FetchedResults<TestPoint> {fetchFail.wrappedValue}
  var fetchedNotTested: FetchedResults<TestPoint> {fetchNotTested.wrappedValue}

  var body: some View {
    NavigationLink(destination: TestPointListView(actionItem: actionItem)) {
      HStack {
        Rectangle()
          .fill(getBackColor(fetchPassCount: self.fetchedPass.count, fetchFailCount: self.fetchedFail.count, fetchNTCount: self.fetchedNotTested.count))
          .frame(width: 10)

        VStack {
          Text(actionItem.wrappedName)

    }

TestPointListView

struct TestPointListView: View {
  @Environment(\.managedObjectContext) var moc

  @ObservedObject var actionItem: ActionItem

  var body: some View {
    List {
      ForEach(actionItem.testPointsArray, id:\.self) {testPoint in
        TestPointListCellView(testPoint: testPoint)

}

TestPointListCellView

struct TestPointListCellView: View {
  @Environment(\.managedObjectContext) var moc

  @ObservedObject var testPoint: TestPoint

  Bunch of code here....

  Button("Save") {
             ...
              do {
                try self.moc.save()
              } catch {
                print(error)
              }
            }
          ...

2      

Hi Daniel

You have a lot going on there and im having a little trouble seeing what is going on without being able to see the full picture. Do you have your code on Github so we can see the full picture?

Just at a quick glance, you have a lot of inits in your Views which set fetch requests. Im still learning the whole process myself but from my understanding working with SwiftUI views you would need to wrap your fetch requests under a @FetchRequest property wrapper so the view will update the changes immediately once any change to the FetchedResults is saves to the moc.

It looks like you are trying to do this by first loading (through the inits) an ObservedObject with your fetch results and initially it would seem like it should work but i think thats where you might be going wrong... maybe.

I would avoid doing this unless you need to do this. Just load your views directly with @FetchRequest, get rid of the inits and classes you have created as Observable objects.

As i said its hard to deduce what is happening without seeing the whole picture so it would be good to see the code if possible.

Dave

2      

Maybe just ket me know what entities you have and what attributes they have as well as relationships and i will try and replicate what is happening.

Dave

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Sorry for the confusion. I was hoping I did something wrong that would be obvious. I'm new, so the inits were the only way I knew how to execute. I have @FetchRequest to get this whole thing started in the Content View that grabs all the checklists I have stored. For the workaround, I couldn't get similar commands to work in the other views and defaulted to the mess of inits above.

I'm having issues posting a pic of the core data model...but with TestGroup<->>ActionItem<->>TestPoint

TestGroup
name
order
phaseOfFlight
Relationships: hasActionItems

ActionItem
name
order
Relationships: belongsToTestGroup, hasTestPoints

TestPoint
name
order
passOrFail
Relationships: belongsToActionItem

I'm building checklists...made up of Test Groups that have Action Items that have specific Test Points. An user creates or selects a stored Checklist. From the list of Test Groups in that Checklist, the user selects one that brings up a master-detail type view of ActionItems (master) and the associated Test Points (detail). ActionItems and TestGroups have colored backgrounds that are defined by the count of passed test points or failed test points.

When the user completes a Test Point, the status (poorly named passOrFail) is changed. I thought since the relationships were defined, the colors would update automatically. But that's not true...I needed to fetch the number of passed, failed, and not tested to update the colors. I'm missing something conceptually.

2      

All good, no need to be sorry, we are all here to learn and help each other. I have been playing around with Core Data a bit recently with SwiftUI and it goes from being easy to hard and back a lot. A few things I have picked up along the way:

From what i have learnt so far, SwiftUI works reasonably well with CoreData at a basic level. To give you a better understanding on how to use it here is a really basic example.

  1. I have created a Entity called People in the CoreData model. It has one attribute called name. I have created the following basic list which displays all the People stored in Core Data. At the top of the list is a TextField to enter a new name and saving it to CoreData. The change will be reflected immediately in the list -
struct CoreDataExample: View {
    @Environment(\.managedObjectContext) var moc // Getting access to the environments CoreData moc
    @FetchRequest(entity: People.entity(), sortDescriptors: []) var people: FetchedResults<People> // Fetch Request to get all the people in CoreData
    @State private var name = "" // variable used to store name entered in textfield by user

    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("Enter name", text: $name)
                    Spacer()
                    Button("Add") { self.save() }
                }
                .padding()
                List {
                    ForEach(people, id: \.self) { person in // Here i am using the array of people from the above FetchRequest to display in the list.
                        NavigationLink(destination: PersonDetail(person: person)) { // I have wrapped each cell in a Nav Link so when user presses it, it will take them to another screen (View below). I have passed the person which will be from the FetchRequest, to this view.
                            Text(person.wrappedName) // Here we are accessing the wrappedName from the person Entity.
                        }
                    }
                }
            }
        }
    }
    // Just a basic save function for saving names added by user in the above textfield. I have kept it simple and not worried about errors etc here.
    func save() {
        let person = People(context: self.moc)
        person.name = name
        try? self.moc.save()
    }
}

The NavigationLink will take you to the following view -

struct PersonDetail: View {
    @Environment(\.managedObjectContext) var moc // Again getting the managedObjectContext

    @FetchRequest(entity: People.entity(), sortDescriptors: []) var people: FetchedResults<People> // Even though we wont be reading from this FetchRequest in this view you need it for the changes to be reflected immediately in your view.

    var person: People // This is the variable that we need to supply from the previous view. This is what we will use to display the name and this is what we will use to update changes to CoreData.

    @State private var newName = ""

    var body: some View {
        VStack {
            Text(person.wrappedName)

            HStack {
                TextField("Enter name", text: $newName)
                Spacer()
                Button("Add") { self.save() }
            }
        }
    }
    // Another save func, but note how i have not created another object using the moc like in the previous view. Because we are just updating CoreData, not creating a new entity

    func save() {
        person.name = newName // Updating the persons name that we passed from the previous view
        try? self.moc.save() // Here we are saving it
    }
}

Just cut and paste this code into XCode and have a look. As I commented in the second view about needing that FetchRequest, delete it, run it and try saving the updated name. You will note that the change wont be reflected in the view immediately, but if you stay in the app, go back to the previous view then back again it will change. Having that FetchRequest will make that change seen immediately without having to go back.

Anyways this is just a basic example. Your situation is a bit more complex but the above fundamentals can be used and extended upon to give you what you need without having custom classes and Observable objects as well as inits for your views. Have a play around and see how you so.

What I recommend is go through and have a look at Paul's tutorials on SwiftUI and Core data. Thats where i initally went and i just played around with CoreData and did some experimenting. Paul's tutorials are good and will give you a solid understanding on how to use Core Data in SwiftUI.

Dave

2      

Dave,

I can't thank you enough. Your advice about Paul's tutorials is spot on...I've picked all this up from his dynamic filtering tutorial for Ed and Tay Tay to the UK candy examples.

What you've described is a MUCH cleaner way of accomplishing what I've done as my workaround. Your line confirms the need to do another fetch in the view:

@FetchRequest(entity: People.entity(), sortDescriptors: []) var people: FetchedResults<People> // Even though we wont be reading from this FetchRequest in this view you need it for the changes to be reflected immediately in your view.

I had hoped that since the var was @ObservedObject (in your example above (var person: People)), any changes to a relationship with that object would work their way up the graph (for lack of a better term).

As a side note, I wish I had seen your code snippet a couple of weeks ago..it demonstrates using textfields and would have solved a couple problems I had that caused me to bash my head into the keyboard for hours...

2      

i would of thought the exact same thing with the ObservedObject, and there probably is a way to do that but it woukd involve a bit of playing around to get it to work and probably a bit more code. Glad i could help Daniel

Dave

2      

hi,

i have had similar fights with CoreData and getting updates to views, but i do have a similar type of project out there if you want to take a look at it (and which doesn't have what looks like an artificial @FetchRequest in it to cause updates). i solved some of these update problems in other ways than Dave has suggested.

-- by the way: great work and sleuthing above, Dave!

take a look at my ShoppingList project. similar ideas: a list of items for a shopping list and a list of locations in a grocery store in which items are found, with a one-to-many relationship that's ShoppingItem << --- > Location. many list views with navigation links to edit views. look for comments in the code about certain update notions, especially for an edit of a Location that needs to be tracked back to all the ShoppingItems associated with the Location.

everything works fine right now, although i keep worrying that one slight tweak will make the whole updating thing break.

hope this helps, and i'm appreciative of any comments you might have.

DMG

2      

DMG, great app, runs smooth. Probably going to use it to devise a plan to avoid people the next time I go to the grocery store.

Looking at the code now. I appreciate the work you did in the individual models and the added functions. I'm also afraid of a "simple" change that will break the entire package.

After the conversation with Dave, I tried some new google searches, and it's amazing how frustration will bring out appropriate search terms. I went looking for @fetchRequest and unnecessary refresh. I found this article that presents a couple solutions. I'm expanding where I need to report the "status" and this is working like a champ.

https://stackoverflow.com/questions/60230251/swiftui-list-does-not-update-automatically-after-deleting-all-core-data-entity/60230873#60230873

I also saw mention of using NotificationCenter. I just used this to change some colors independent of Core Data and it also worked well. Need to find more tutorials though.

https://stackoverflow.com/questions/58643094/how-to-update-fetchrequest-when-a-related-entity-changes-in-swiftui

Then I saw articles about incorporating Combine. Those made my head explode and have to do more research.

I'm also making use of .filter on the fetchedResults arrays. I don't know if there's any savings in that method vice fresh fetchrequests.

If you two don't mind, I'll keep this thread as unsolved even though you provided great answers. I'm still surprised @ObservedObject didn't work as expected and will continue to update as I find other methods.

3      

Sounds good Daniel. I also had a look at the Shopping List app and nice work DMG. I am developing an app at the moment which uses CoreData hence the sleuthing i have done regarding it. I think as Paul mentions in one of his tutorials that CoreData was introduced way back and since there have not been many updates to it since which is why we are having to discover this ourselves. In a way its a good thing because it helps us learn and understand CoreData. That being said i hope at the next WWDC we see some updates with CoreData and its integration into the current climate of SwiftUI and upcoming iOS 14. It would be good to leave this thread open and see if anyone else has come up with ways to achieve the desired results.

Dave

2      

Dave and Daniel,

thanks for your comments above, and i also would like to leave this thread open.

i've done some updating for my experimental ShoppingList app, especially cleaning up some code and adding lots of comments. in particular, i now have some code in there for sectioning out a list in GroupedListStyle(), and the visual updating does seem to work right. and i have fixed a few bugs and a crash or two.

as for this on-going discussion, i think issues involving CoreData and SwiftUI are not so much because of the age of CoreData and its Objective C heritage, but the particular way that these two interact as a result of the @FetchRequest construct in SwiftUI.

@FetchRequest looks like the SwiftUI equivalent of having a UITableView with a built-in NSFetchedResultsController in UIKit -- and so lots of magic happens under the hood for you. my problem is that i don't know exactly what that magic does or when it does it. that's why when i change something in one place, i wonder whether it will really have an updated visual appearance in the other place.

hope that's useful,

DMG

2      

Thank you so so much for this thread guys (Daniel), you've saved me from some serious headaches!

Adding the FetchRequest to the child view 🤦‍

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.