#swift #ios #core-data #thought Recently I had to re-familiarize myself with [[CoreData]], the built in framework by Apple for general DB abstractions. It has been a few years since I used [[CoreData]] and only recalled the API being clunky at the time. To prepare I started out with a [[WWDC#^6800a1|WWDC: Making Apps With CoreData]] video and that had a ton of updated information on the new APIs like `NSPersistentContainer` which makes it extremely easy to setup a data stack without a ton of boilerplate. One thing I wanted to get out of this as well was a simple way of transforming the typical sync/async [[CoreData]] operations into [[Combine]] publishers. Most of the time these operations are performed in [[ViewModel]]s or other business logic parts of an application, in my case my app uses [[Combine]] heavily. ## Setting up NSPersistentContainer ```swift // The container used to interface with the entire CoreData stack let container: NSPersistentContainer // The main thread context for performing operations let context: NSManagedObjectContext // A default background thread context for performing operations let backgroundContext: NSManagedObjectContext container = NSPersistentContainer(name: "DataModel") // Required: loads the store (sync) container.loadPersistentStores { _, _ in } context = container.viewContext backgroundContext = container.newBackgroundContext() ``` ## Saving changes To save changes to the context such as deletes, appends, updates, etc. The API provides `save() throws` to save the changes made to the context. ```swift // Saves the current context do { try context.save() } catch { print(error) } ``` ## Fetching data A simple `fetch<T>(_ request:)` below makes it easy to transform an [[NSFetchRequest]] into a [[Combine]] publisher. ```swift // Fetches a generic managed object and returns a publisher func fetch<T: NSFetchRequestResult>( _ request: NSFetchRequest<T> ) -> AnyPublisher<[T], Error> { Deferred { Future { promise in do { let result = try backgroundContext.fetch(request) promise(.success(result)) } catch { promise(.failure(error)) } } } .eraseToAnyPublisher() } ``` ## DataStore We can combine these pieces into a simple reusable interface `DataStore` ```swift import Combine import CoreData import Foundation protocol DataStoring { var container: NSPersistentContainer { get } var context: NSManagedObjectContext { get } var backgroundContext: NSManagedObjectContext { get } func fetch<T: NSFetchRequestResult>( _ request: NSFetchRequest<T> ) -> AnyPublisher<[T], Error> } struct DataStore: DataStoring { let container: NSPersistentContainer let context: NSManagedObjectContext let backgroundContext: NSManagedObjectContext init() { container = NSPersistentContainer(name: "DataModel") container.loadPersistentStores { _, _ in } container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump container.viewContext.automaticallyMergesChangesFromParent = true context = container.viewContext backgroundContext = container.newBackgroundContext() backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump backgroundContext.automaticallyMergesChangesFromParent = true } func fetch<T: NSFetchRequestResult>( _ request: NSFetchRequest<T> ) -> AnyPublisher<[T], Error> { Deferred { Future { promise in do { let result = try backgroundContext.fetch(request) promise(.success(result)) } catch { promise(.failure(error)) } } } .eraseToAnyPublisher() } } ``` For testing you could then create a `MockDataStore` which would be injected into the consumers of the data store. ## More reading - [[Using NSFetchRequestController]] - [[Observing NSManagedObjectChanges]]