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

Using Core Data and MVVM, view not updating automatically

Forums > SwiftUI

I'm having some challenges using Core Data in an MVVM architecture. I want to display a list of activities. I am able to display the list, however it doesn't automatically update when new data is added.

class ActivityListViewModel: NSObject, ObservableObject {
    @Published var activities = [ActivityViewModel]()

  private let fetchedResultsController: NSFetchedResultsController<Activity>

  init(storageProvider: StorageProvider) {
      let request: NSFetchRequest<Activity> = Activity.fetchRequest()
      request.sortDescriptors = [NSSortDescriptor(keyPath: \Activity.title, ascending: true)]

      self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: storageProvider.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)

      super.init()

      fetchedResultsController.delegate = self
      try! fetchedResultsController.performFetch()
      let activities = fetchedResultsController.fetchedObjects ?? []
      self.activities = activities.map(ActivityViewModel.init)
  }

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
      if let newActivities = controller.fetchedObjects as? [Activity] {
          self.activities = newActivities.map(ActivityViewModel.init)
      }
  }
}

The published activities are ActivityViewModels (see struct below), not Activities. These are converted in the initializer. I believe this is what prevents ContentView from updating the list automatically as it works as expected if I use the normal Activity class instead of the ActivityViewModel struct.

struct ActivityViewModel: Identifiable {
    private var activity: Activity

  init(activity: Activity) {
      self.activity = activity
  }

  let id = UUID()

  var title: String {
      return activity.title ?? ""
  }
}
struct ContentView: View {
    @ObservedObject var viewModel: ActivityListViewModel

  var body: some View {
      List {
          ForEach(viewModel.activities) { activity in
              Text(activity.title)
          }
      }
  }
}

1      

I am having a similar problem. My child view updates coredata being observed by parent, but changes to the coredata by childview do not cause the parent view to be re-drawn. Will be watching to see what you learn.

1      

hi Jordy,

i am puzzled by this ... it seems that adding new activities should certainly trigger an update, since your viewModel.activities is a @Published property in an @ObservedObject and this array will change with any update. if using Core Data objects directly (rather than structs that front those objects) does work, it becomes more mysterious.

however, i am more curious about your choice of fronting Core Data objects with structs. with every updated Activity object, you're (eventually) kicking the NSFetchedResultsController, which then rewrites all of the structs vended by the ActivityListViewModel, using a completely new set of ids (of type UUID). i wonder if this somehow confuses SwiftUI, especially since Swift synthesizes support of Identifiable for Core Data objects already in a different way.

so, i would ask: is it necessary to front the Core Data objects with structs at all?

if all you're trying to do is soften the translation of the title coming from Core Data to nil-coalesce it nicely for SwiftUI, i would suggest just adding an extension on the Activity class to do this directly:

extension Activity {
  var niceTitle: String { title ?? "" }
}

i am not sure if this will help ... i would not mind having a chance to see more of your code to experiment a little ... but maybe this id issue is at the heart of it?

regards,

DMG

1      

Is your ActivityListViewModel based on Donny Wals' Core Data book, under the heading "Using a fetched results controller in SwiftUI"?

As you say, that seems to be fine, but the problem lies in using your struct instead of the properties of the Activity class maintained by Core Data. If you got that approach somewhere, I'd be interested in the URL to understand the philosophy.

For Text views, it's not that cumbersome to add nil-coalescing at every occurrence: Text(activity.title ?? "")

If you need a 2-way binding, like a TextField, Donny Wals' book has a great solution: an extension to the Binding property wrapper type that allows you to seamlessly use bindings on the actual properties of any Core Data entity. It would be copyright infringement to reproduce it here, so I encourage people to buy his $35 eBook, then search for the text "extension Binding". https://www.donnywals.com

1      

Jerry, me again, my code updates and accesses the core data directly, which I though would trigger a redraw.

Been trying different approaches for two weeks now.

Bob - I'll check into the Wals book as sounds like it might be helpful in understanding/addressing some of the errors I have run into when looking for a workaround.

Lastly, If I am toggling an observed object from View B, and View A also is observing the same observable object, if B changes the value, shouldn't A re-render?

1      

Sandy: Yes, view A should re-render, so there may be an issue with whether you really are modifying the observed object. Try putting a print statement in the controllerDidChangeContent() closure of both view models A and B.

1      

I revisited some of Paul's tutorials on Core Data. In SwiftUI by Example, he's quite emphatic that SwiftUI should use @FetchRequest instead of NSFetchedResultsController because that approach is used exclusively in Apple's documentation and videos. In his Hacking with SwiftUI books for iOS and Mac, he uses only @FetchRequest. https://www.hackingwithswift.com/quick-start/swiftui/introduction-to-using-core-data-with-swiftui

@FetchRequest certainly simplifies the code and automatically updates the view. So perhaps it would be good to try it before expending more effort on NSFetchedResultsController.

For a contrary opinion, Donny Wals' book covers both @FetchRequest and NSFetchedResultsController for SwiftUI, but he prefers the latter. Most of the relevant portion of his book (but not his clever extension to Binding) is in this blog post: https://www.donnywals.com/fetching-objects-from-core-data-in-a-swiftui-project/

1      

Bob - Yes, I indeed got some inspiration from Donny Wals' book. I didn't get to the binding section you mentioned, so perhaps I just need to keep reading.

DMG - an extension of the Core Data object classes to deal with the optional values would definitly work. The goal for this project is to get more experienceMVVM design pattern.

1      

hi Jordy, Bob, Sandy,

for Jordy: i think both Bob and i are interested in how your "struct fronts Core Data object" idea works out. (i certainly am.) if you have code to share out on GitHub, it would be of interest to each of us.

added after posting, for Bob

  • i think there are differences of opinion on the use of @FetchRequest over using Donny Wals's approach of using an NSFetchedResultsController. it may simply come down both to programming style and to a particular use case as to which is best. a recent project i consulted on required multiple configurations in the Core Data model (a local store for pre-loaded data, and a cloud store for user-generated data), but we wanted to vend objects to client views without them knowing from which store the objects came. you could not do that with @FetchRequest.

and just as a note to Sandy, you asked:

If I am toggling an observed object from View B, and View A also is observing the same observable object, if B changes the value, shouldn't A re-render?

if you mean "toggling a Bool property of the object," then yes, it should re-render when both views A and B hold @ObservedObject references to the same Core Data object.

but, this may be where you want to start a new topic and show a little more code, especially since you've been struggling more with a parent-child update problem than the one above with Jordy. such a situation often involves Core Data relationships, where things you might expect to happen may not always be as you think.

good luck all, and i'll continue reading, hoping to help as usual,

DMG

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.