22 Replies
      Latest reply on Aug 7, 2019 11:15 AM by Skipjakk
      rlovelett Level 1 Level 1 (0 points)

        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.

        • Re: Core Data Model SwiftUI
          sfcoder Level 1 Level 1 (0 points)

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

          • Re: Core Data Model SwiftUI
            Lightandshadow Level 1 Level 1 (0 points)

            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)
                }
            }
              • Re: Core Data Model SwiftUI
                madcat Level 1 Level 1 (0 points)

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

                • Re: Core Data Model SwiftUI
                  Djamesbell Level 1 Level 1 (0 points)

                  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?

                    • Re: Core Data Model SwiftUI
                      Allow2 Level 1 Level 1 (0 points)

                      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?

                    • Re: Core Data Model SwiftUI
                      IngmarStein Level 1 Level 1 (15 points)

                      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?

                        • Re: Core Data Model SwiftUI
                          _hs_ Level 1 Level 1 (0 points)

                          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

                        • Re: Core Data Model SwiftUI
                          _hs_ Level 1 Level 1 (0 points)

                          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.

                            • Re: Core Data Model SwiftUI
                              defn Level 1 Level 1 (0 points)

                              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.

                          • Re: Core Data Model SwiftUI
                            Skipjakk Level 1 Level 1 (0 points)

                            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.

                              • Re: Core Data Model SwiftUI
                                Allow2 Level 1 Level 1 (0 points)

                                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?

                                  • Re: Core Data Model SwiftUI
                                    Skipjakk Level 1 Level 1 (0 points)

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

                                    • Re: Core Data Model SwiftUI
                                      franc Level 1 Level 1 (0 points)

                                      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.


                                        • Re: Core Data Model SwiftUI
                                          Allow2 Level 1 Level 1 (0 points)

                                          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?

                                    • Re: Core Data Model SwiftUI
                                      Zouritte Level 1 Level 1 (0 points)

                                      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

                                      • Re: Core Data Model SwiftUI
                                        fkdev Level 1 Level 1 (10 points)

                                        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.

                                          • Re: Core Data Model SwiftUI
                                            caram Level 1 Level 1 (0 points)

                                            @fkdev, example code would be great indeed!

                                              • Re: Core Data Model SwiftUI
                                                fkdev Level 1 Level 1 (10 points)

                                                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)")
                                                        }
                                                    }
                                                }
                                                  • Re: Core Data Model SwiftUI
                                                    sverin Level 1 Level 1 (0 points)

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

                                              • Re: Core Data Model SwiftUI
                                                SpaceMan Level 1 Level 1 (10 points)

                                                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
                                                }()
                                                • Re: Core Data Model SwiftUI
                                                  Skipjakk Level 1 Level 1 (0 points)

                                                  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
                                                      }
                                                  }