Swift Repository pattern using Core Data

What Is Core Data?
Core Data, a framework that's part of the iOS SDK, assists developers by allowing them to store their model objects into an object graph using.
While it may prove tryicky to master at first, it helps developers with built-in features such as:
- Change tracking
- Relationships between model objects
- Lazy loading of objects
- Property validations
- Automatic schema migrations
- Advanced querying

As soon as you've defined your model classes, preferably in the Core Data scheme editor interface provided by Xcode, you can set up a Core Data stack like this:
import CoreData
class CoreDataContextProvider {
// Returns the current container view context
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
// The persistent container
private var persistentContainer: NSPersistentContainer
init(completionClosure: ((Error?) -> Void)? = nil) {
// Create a persistent container
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
completionClosure?(error)
}
}
// Creates a context for background work
func newBackgroundContext() -> NSManagedObjectContext {
return persistentContainer.newBackgroundContext()
}
}
And to create your first Core Data managed object:
let book = NSEntityDescription.insertNewObject(forEntityName:"Book", in: managedObjectContext)
If you are not familiar with the framework, you can check Apple's Getting Starter Guide.
Why?
Core Data has great support for UIKit but often it is a good idea to create an abstraction layer between the business layer and the storage layer.
If you need to display data from Core Data straight into a UITableView I suggest you go straight to NSFetchedResultsController because it provides great support for it.
Otherwise, when working with CoreData, the code you have to write can be quite verbose and it often repeats, the only things that vary are the models you query for most of the times therefore the Repository and Unit of Work patterns are great choices for creating an abstraction layer between the domain layer and the storage layer.
Instead of this:
let moc = coreDataContextProvider.viewContext
let bookFetch = NSFetchRequest(entityName: "Book")
do {
let fetchedBooks = try moc.executeFetchRequest(bookFetch) as! [BookMO]
} catch {
fatalError("Failed to fetch books: \(error)")
}
We would much rather prefer to write this:
let result = booksRepository.getAll()
Using the repository pattern we can easily mediate between the domain layer and the data layer while the unit of work pattern coordinates the write transactions to Core Data.
Creating a generic repository
As I previously mentioned, we want to remove code duplication and to do that, we have to create a generic repository that can perform the same operations on any NSManagedObject
subclass. To start, let's define a protocol to see how that repository will look like.
protocol Repository {
/// The entity managed by the repository.
associatedtype Entity
/// Gets an array of entities.
/// - Parameters:
/// - predicate: The predicate to be used for fetching the entities.
/// - sortDescriptors: The sort descriptors used for sorting the returned array of entities.
func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error>
/// Creates an entity.
func create() -> Result<Entity, Error>
/// Deletes an entity.
/// - Parameter entity: The entity to be deleted.
func delete(entity: Entity) -> Result<Bool, Error>
}
The classes implementing this protocol, regardless if they are working with Core Data, UserDefaults or even an API service, will have to specify the model it is working with. This model is defined with the help of an associated type called Entity
.
Last but not least, we want our methods to return Results that consist of an entity for creation, an array of results for a query and a boolean if a deletion was successful.
Now let's write a concrete implementation of how a generic Core Data repository:
/// Enum for CoreData related errors
enum CoreDataError: Error {
case invalidManagedObjectType
}
/// Generic class for handling NSManagedObject subclasses.
class CoreDataRepository<T: NSManagedObject>: Repository {
typealias Entity = T
/// The NSManagedObjectContext instance to be used for performing the operations.
private let managedObjectContext: NSManagedObjectContext
/// Designated initializer.
/// - Parameter managedObjectContext: The NSManagedObjectContext instance to be used for performing the operations.
init(managedObjectContext: NSManagedObjectContext) {
self.managedObjectContext = managedObjectContext
}
/// Gets an array of NSManagedObject entities.
/// - Parameters:
/// - predicate: The predicate to be used for fetching the entities.
/// - sortDescriptors: The sort descriptors used for sorting the returned array of entities.
/// - Returns: A result consisting of either an array of NSManagedObject entities or an Error.
func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error> {
// Create a fetch request for the associated NSManagedObjectContext type.
let fetchRequest = Entity.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = sortDescriptors
do {
// Perform the fetch request
if let fetchResults = try managedObjectContext.fetch(fetchRequest) as? [Entity] {
return .success(fetchResults)
} else {
return .failure(CoreDataError.invalidManagedObjectType)
}
} catch {
return .failure(error)
}
}
/// Creates a NSManagedObject entity.
/// - Returns: A result consisting of either a NSManagedObject entity or an Error.
func create() -> Result<Entity, Error> {
let className = String(describing: Entity.self)
guard let managedObject = NSEntityDescription.insertNewObject(forEntityName: className, into: managedObjectContext) as? Entity else {
return .failure(CoreDataError.invalidManagedObjectType)
}
return .success(managedObject)
}
/// Deletes a NSManagedObject entity.
/// - Parameter entity: The NSManagedObject to be deleted.
/// - Returns: A result consisting of either a Bool set to true or an Error.
func delete(entity: Entity) -> Result<Bool, Error> {
managedObjectContext.delete(entity)
return .success(true)
}
}
By writing this generic class, we can now create repositories that handle NSManagedObjects
like this:
let bookRepository = CoreDataRepository<BookMO>(managedObjectContext: context)
let bookResult = bookRepository.create()
switch result {
case .success(let bookMO):
bookMO.title = "The Swift Handbook"
case .failure(let error):
fatalError("Failed to create book: \(error)")
}
context.save()
Often it's a good idea to separate your domain models from your storage models. Your domain models should not be concerned with any aspect of how the data is stored in Core Data. Maybe at some point, you want to break a model into smaller models because you need to improve the Core Data query performance, maybe you want to drop Core Data and use Realm. Nonetheless, the domain layer should not interact with NSManagedObjects directly.
Now let's build a concrete domain facing repository on top of our existing generic one that handles domain objects instead:
/// Protocol that describes a book repository.
protocol BookRepositoryInterface {
// Get a gook using a predicate
func getBooks(predicate: NSPredicate?) -> Result<[Book], Error>
// Creates a book on the persistance layer.
func create(book: Book) -> Result<Bool, Error>
}
// Book Repository class.
class BookRepository {
// The Core Data book repository.
private let repository: CoreDataRepository<BookMO>
/// Designated initializer
/// - Parameter context: The context used for storing and quering Core Data.
init(context: NSManagedObjectContext) {
self.repository = CoreDataRepository<BookMO>(managedObjectContext: context)
}
}
extension BookRepository: BookRepositoryInterface {
// Get a gook using a predicate
@discardableResult func getBooks(predicate: NSPredicate?) -> Result<[Book], Error> {
let result = repository.get(predicate: predicate, sortDescriptors: nil)
switch result {
case .success(let booksMO):
// Transform the NSManagedObject objects to domain objects
let books = booksMO.map { bookMO -> Book in
return bookMO.toDomainModel()
}
return .success(books)
case .failure(let error):
// Return the Core Data error.
return .failure(error)
}
}
// Creates a book on the persistance layer.
@discardableResult func create(book: Book) -> Result<Bool, Error> {
let result = repository.create()
switch result {
case .success(let bookMO):
// Update the book properties.
bookMO.identifier = book.identifier
bookMO.title = book.title
bookMO.author = book.author
return .success(true)
case .failure(let error):
// Return the Core Data error.
return .failure(error)
}
}
}
This will allow us to pass domain model objects and store, update or delete them from the persistence layer. The Book
type in this example is a domain model that can look like this:
struct Book {
let identifier: String
let title: String
let author: String
}
While our BookMO class is our persistence facing model class:
import Foundation
import CoreData
@objc(BookMO)
public class BookMO: NSManagedObject {
}
extension BookMO {
@nonobjc public class func fetchRequest() -> NSFetchRequest<BookMO> {
return NSFetchRequest<BookMO>(entityName: "BookMO")
}
@NSManaged public var identifier: String?
@NSManaged public var title: String?
@NSManaged public var author: String?
}
To help with the transform from a persistence model to a domain model, we can define a generic protocol to help us with that:
protocol DomainModel {
associatedtype DomainModelType
func toDomainModel() -> DomainModelType
}
And for our BookMO class to implement this to facilitate the creation of a Book model we'll have to do the following:
extension BookMO: DomainModel {
func toDomainModel() -> Book {
return Book(identifier: identifier,
title: title,
author: author)
}
}
Now we can easily persist a user's favourite book into Core Data by writing the followin line:
let book = Book(...)
bookRepository.create(book: book)
Unit of work
The unit of work pattern will help us keep track of eveything that needs to be done in a single save transaction. For example, after creating a book, we might want to add it to a library but if the library is no longer present we want to revert everything.
Another practical aspect of a unit of work is that it can help us with unit testing our implementation in-memory context for example.
class UnitOfWork {
/// The NSManagedObjectContext instance to be used for performing the unit of work.
private let context: NSManagedObjectContext
/// Book repository instance.
let bookRepository: BookRepository
/// Designated initializer.
/// - Parameter managedObjectContext: The NSManagedObjectContext instance to be used for performing the unit of work.
init(context: NSManagedObjectContext) {
self.context = context
self.bookRepository = BookRepository(context: context)
}
/// Save the NSManagedObjectContext.
@discardableResult func saveChanges() -> Result<Bool, Error> {
do {
try context.save()
return .success(true)
} catch {
context.rollback()
return .failure(error)
}
}
}
To use the unit of work we will have to do the following:
let unitOfWork = UnitOfWork(context: newBackgroundContext)
unitOfWork.bookRepository.create(book: book)
unitOfWork.saveChanges()
As we usually have to decide on which context we offload the Core Data interaction, be it a background context for some data downloaded from an API or the view context for fetching data from the persistent store, the unit of work can take care of that for us.
At the same time, it provides the same context to the encapsulated repositories to make sure we don't mix contexts when dealing with multiple queries and inserts.