#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]]