Core Data Model SwiftUI

Are there any best practices or sample code provided for SwiftUI and Core Data?


Specifically, using a Core Data NSManagedObject as a data provider of a View. Is it a @ObjectBinding? How do you handle the PreviewProvider? If I'm passing in a Core Data object presumably I need a whole context and store coord to instantiate.

I would also appreciate sample code, as the sample for Core Data and CloudKit is not using SwiftUI.

The most likely recommended strategy is to create a view model for each item you want to pass to your SwiftUI view, along with a datasource class with properites for a PassthroughDataSource, NSFetchedResultsController and an array of view models.


Set your datasource as the results controller delegate and configure your results contrller fetch predicate. Your view model should have an initalizer that takes an NSManagedObject and sets the properties you want to display in your UI. When the delegte method is called, enumerate over the results, create view models initalized with the resulting core data objects, append them to the array, then call send(.self) on your PassThroughSubject.


I usually add a createSampleData() method that populates the same array with hard coded view models initalized with test data for use with the preview provided.


class MyManagedObject: NSManagedObject {
   
    @NSManaged var name: String?
    @NSManaged var age: NSNumber?
}

struct MyViewModel {
   
    var name: String
    var age: String
   
    init(managedObject: MyManagedObject) {
        self.name = managedObject.name ?? ""
        self.age = "\(managedObject.age)" ?? ""
    }
}

final class QueryListStore: NSObject, BindableObject, NSFetchedResultsControllerDelegate {
   
    var didChange = PassthroughSubject<queryliststore, never="">()
    var results = [MyViewModel]()
    var controller = NSFetchedResultsController()
   
    override init() {
       
        super.init()
        /* Create and configure fetched results controller */
       
        controller.delegate = self
       
        do {
            try controller.performFetch()
        } catch {
            fatalError("Failed to fetch entities: \(error)")
        }
    }
   
    func controllerDidChangeContent(_ controller: NSFetchedResultsController) {
       
        var viewModels = [MyViewModel]()
       
        /* Iterate over results, initalizing view models from your managed objects and adding them to the array
        
         let viewModel = MyViewModel(managedObject: managedObject)
         viewModels.append(viewModel)
        
         */
       
        self.results = viewModels
       
        didChange.send(self)
    }
}

Thanks, for your code. I could not figure out, where to integrate QueryListStore.

i’m not that familiar with Core Data but wouldn’t the code example above fault all the managed objects returned in the query And build a ViewModel for each, so only useful for small data sets? Any thoughts on how to do this but keep batch faulting machinism?

That seems to be recommended approach for read-only use cases. But what if you wanted to have a binding to a property in the managed object to be mutated in the UI?

The framework is a mix of Microsoft's MVVM and Facebook's DOM-based React framework.

- Based on the recommended approach (XCode Preview session), you should not mutate the Core-Data Entity.

- Instead, the bindings to the ViewModel triggers update of the UI.

- This also makes sure that your UI is dependent on design time ViewModel (and not run time core-data)


= Better testing, better preview support, better maintenance.


The big unknown is how performant SwiftUI will be in the real word

Good solution.

The changes I would suggest is adding id to the ViewModel to make it unique and using didSet to update the backing store.

There are some examples of using Core Data with SwiftUI showing up on github and the web:


https://github.com/StephenMcMillan/Dub-Dub-Do


https://github.com/italoboss/EmotionalDiary


https://medium.com/@rosscoops/swiftui-nsfetchedresultscontroller-f9f27718e3d4


They seem to work with simple models (one entity), haven't seen any code based on more complex models (multi-entity with relationships) yet, nor have I been able to get a more complex model to work.

Which part doesn’t work on complex entities/joins? I am migrating a very complex app over and am fairly comfortable SwiftUI will Carr for most cases so far, have some unexplored notification and navigation things, plus sig in with Apple etc I still need to work out.


but is it the fact you cannot map the data to a proxy object? Or Is the fetch not triggering update on the nested entities? Or something else, like it just fundamentally “breaks”?


I am just starting to think this one through so starting to work out how to handle this.


My current thinking is 2-fold. The first is part is it appears you need to treat the data preparation and collation very much like redux. But this is of course a concern for large data sets. Out app generally doesn‘t show more than a handful of rows on any one biew a time generally, so thats not really a current concern. It appears the long term goal MAY be for nsfetchedresultscontroller, or a derivative/replacement to offer some layer of direct usage and potentially more support for Combine to cache/etc, or it may already be doing that in the background.

it appears the current concern is the need to translate all the objects into proxy objects which gets us the whole faulting in issue.


Anyone else got some other advice or resources to contribute?

you wouldn’t think it would be too hard right? The array coming back from the nsfetchedresults controller is just an array, and the “swift ui” code is not potentially literally executed in that fashion. It COULD be the internal abstraction in use in the translation essentially uses the closure as a body to render a give cell and it only calls the body when required? Has anyone actually tried creating a List with 40,000 elements and put a print in the closure to see if they are all actually calculated?


surely the abstraction isn’t stupid enough to actually attempt to render all rows?


Yet, without a cell height hint/estimate, then maybe it absolutely must do so for now? Otherwise, how would it now the range of visible cells?

Hi, i’m As well looking forward to seeing the Apple best approach of the CoreData integration with SwiftUI. Anyone of you have found an Apple tutoriel or code on how to do it, properly ? Best Tim

I've just been having problems trying to sort passed through data.

Could your View Model have no storage and simply have getters and setters that connect the view to the core data model doing any processing in real time as the data passes through, thereby avoiding the faulting problem?
EDIT: Well, that doesn't work, List still calls all of them firing all the faults.

Sorry for asking this newbie question. Let's consider a basic contact list app:


Say I have a list of "Row" elements displaying only a photo and First Name. Tapping on a row will take you to a "Detail" page that shows other data like last name and date of birth.


  1. Am I supposed to create two separate viewmodels - one for the "Row" view and another for the "Detail" view?
  2. If so, should I be initialising two separate arrays of these when the app loads (and the dataStore gets initialised in SceneDelegate.swift)?
  3. When a user is in editing mode in the "Detail" page, is the SwiftUI View "allowed' to call methods in the dataStore class directly? It seems very easy to do this with when I can set the dataStore as an @EnvironmentObject variable.


I'm super new to MVVM and would appreciate any form of guidance.

Here is a method which seems to work with Xcode 11 Beta 4. I have created 50000 and displayed them in a List by binding the result of a fetch request without having the 50000 faults being fired.


I use SwiftUI List with the .objectIDproperty as id instead of implementing Identifiable:


List(myStore.arrayOfManagedObjects, id:\.objectID)

Then in my cell View, I fire the fault in onAppear by accessing a property of the NSManagedObject.


My NSManagedObject implements BindableObject so that when it is realized from database (awakeFromFetch), willchange is send and the cell view is refreshed.


It seems to work. Will post code later if requested.

@fkdev, example code would be great indeed!

I created a class of lazy variables, each of which fetches an entity. I then use that in a SwiftUI view and iterate over the elements of trhe entity array, passing the element into the detailed view.


lazy var myentitys: [MyEntity]? = {
        guard let appDelegate =
            UIApplication.shared.delegate as? AppDelegate else {
                return nil
        }
        let managedContext = appDelegate.persistentContainer.viewContext
        let request: NSFetchRequest = MyEntity.fetchRequest()
        let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
        request.sortDescriptors = [sortDescriptor]
        if let theEntities = try? managedContext.fetch(request) as [MyEntity] {
            return theEntities
        }
        return nil
}()

Thanks for checking that. I gather it must be a side-effect of the cell height problem.


Would this be pointing to a "best practice" to paginate the results and trigger data on scroll (modifying the query)? I mean a huge scrolling list is not uber practical right?

I have pasted a lot of code below but the secret sauce here is to use .objectID in line 43 as an identifier instead of your own attribute so that the faults are not fired until the cell is displayed via onAppear at line 21.

Below I am using the fetchedObjects property from NSFetchedResultsController but it would work with the result array of a simple query.




//MARK: The Model
public class POI: NSManagedObject, BindableObject {
    public let willChange = PassthroughSubject<void, never="">()
    public override func awakeFromFetch() {
        print("\(name) realized")
        self.willChange.send()
    }
    
    public func fire() {
        let _ = self.name
    }
}

//MARK: The cell
struct POIRow: View {
    @ObjectBinding var poi: POI

    var body: some View {
        HStack {
            Text(name)
        }.onAppear { self.poi.fire() }
    }
    
    var name:String {
        let name:String
        if poi.isFault {
            name = "Please wait..." //never displayed in practice
        } else if let poiname = poi.name {
            name = poiname
        } else {
            name = "no name, no slogan" //should not happen
        }
        return name
    }
}

//MARK: The view
struct TestView : View {
    @ObjectBinding var store:POIStore = POIStore(context:(UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext)

    var body: some View {
        NavigationView {
            List(store.places, id:\.objectID) { (poi:POI) in 
                POIRow(poi:poi)
            }
            .onAppear {
                self.store.performFetch()
            }
        }
    }
}

#if DEBUG
struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}
#endif

 class POIStore: NSObject, BindableObject, NSFetchedResultsControllerDelegate {
    let willChange = PassthroughSubject<void, never="">()
    let context:NSManagedObjectContext
    
    init(context:NSManagedObjectContext) {
        self.context = context
    }
    
    var places:[POI] {
        return self.fetchedResultsController.fetchedObjects ?? [POI]()
    }

//MARK: NSFetchedResultController
    fileprivate lazy var fetchedResultsController: NSFetchedResultsController = {
        let fetchRequest: NSFetchRequest = POI.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultsController.delegate = self
        return fetchedResultsController
    }()
    
    func controllerWillChangeContent(_ controller: NSFetchedResultsController) {
        self.willChange.send()
    }
    func performFetch() {
        do {
            try self.fetchedResultsController.performFetch()
            self.willChange.send()
        } catch {
            fatalError("Failed to fetch entities: \(error)")
        }
    }
}

When I try to use this solution in Xcode 11 Beta 4, I receive an error with this code:


List(store.items, id:\.objectID) { item in
    NavigationLink(destination: Detail(item: item)) {
        Row(item: item)
    }
}.onAppear {
    self.store.performFetch()
}


"UITableView was told to layout its visible cells and other contents without being in the view hierarchy. ..." and

"Invalid update: invalid number of sections. ..."


However, changing the code to add a 0.1s delay before fetching fixes the issue:


List(store.items, id:\.objectID) { item in
    NavigationLink(destination: Detail(item: item)) {
        Row(item: item)
    }
}.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        self.store.performFetch()
    }
}


I only have about 10 items in my store so the time for the fetch is very small. Could it be that your 50000 row fetch takes more than my 1ms delay so you don't experience this problem? Is there a better way to execute the fetch than "onAppear".

I don't reproduce the problem in the simulator with 10 or less items and I am short on devices to run the betas.

That said, I am not sure where the fetch should be done, maybe "onAppear" is not the best place. Originally I took it from a sample here:

https://mecid.github.io/2019/07/03/managing-data-flow-in-swiftui/


Maybe, the functional way would be to do it lazily when the fetched results controller is accessed.

With the new @FetchRequest property wrapper, core data fetches can occur directly in the view, per below forum discussion:


https://forums.developer.apple.com/message/374407#374407


SceneDelegate:

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let context = appDelegate.persistentContainer.viewContext
       
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: ContentView().environment(\.managedObjectContext, context))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

ContentView:

import SwiftUI
import CoreData

struct ContentView: View {

    @Environment(\.managedObjectContext) var managedObjectContext 
    @FetchRequest(fetchRequest: fetchRequest(), animation: nil) var people : FetchedResults
   
    var body: some View {
        List (people, id: \.objectID) {person in
            Text("\(person.lastname ?? "")")
            Text("\(person.firstname ?? "")")
        }
    }
   
    //Core Data Fetch Request
    static func fetchRequest() -> NSFetchRequest {
     let request : NSFetchRequest = Person.fetchRequest()
     request.sortDescriptors = [NSSortDescriptor(key: "lastname", ascending: false)]
     return request
    }
}

From the Core Data template (XCode 11), if I change the Event object to update the timestamp property from the detail view, the list in the master view is refreshed (and correctly display the event updated timestamp), but the visible content in the detail view is not (the Text displaying the formatted timestamp). Do you know why?

blckbirds.com/post/core-data-and-swiftui

This is a good tutorial to start with. It was written just a couple of months back.
Core Data Model SwiftUI
 
 
Q