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.

Replies

@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.