Repository pattern using Core Data and Swift
Core Data is a framework that is included in the iOS SDK and helps developers persist their model objects into an object graph.
This helps developers with built-in features such as:
While it may prove tricky to master at first, it is worth investing time in mastering it.
As soon as you defined your model classes, preferably in the Core Data scheme editor interface provided by Xcode, you can set up a Core Data stack. This is how you can do it:
1 import CoreData
2 class CoreDataContextProvider {
3 // Returns the current container view context
4 var viewContext: NSManagedObjectContext {
5 return persistentContainer.viewContext
6 }
7 // The persistent container
8 private var persistentContainer: NSPersistentContainer
9 init(completionClosure: ((Error?) -> Void)? = nil) {
10 // Create a persistent container
11 persistentContainer = NSPersistentContainer(name: "DataModel")
12 persistentContainer.loadPersistentStores() { (description, error) in
13 if let error = error {
14 fatalError("Failed to load Core Data stack: \(error)")
15 }
16 completionClosure?(error)
17 }
18 }
19 // Creates a context for background work
20 func newBackgroundContext() -> NSManagedObjectContext {
21 return persistentContainer.newBackgroundContext()
22 }
23 }
And then to create your first Core Data managed object:
1 let book = NSEntityDescription.insertNewObject(forEntityName:"Book", in: managedObjectContext)
If you are not familiar with the framework, you can check Apple's Getting Starter Guide.
Core Data has great support for UIKit but often it might be a good idea to create an abstraction layer between the business and the storage layer.
When you need to display data from Core Data straight into a UITableView, my suggestion is to 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:
1 let moc = coreDataContextProvider.viewContext
2 let bookFetch = NSFetchRequest(entityName: "Book")
3 do {
4 let fetchedBooks = try moc.executeFetchRequest(bookFetch) as! [BookMO]
5 } catch {
6 fatalError("Failed to fetch books: \(error)")
7 }
It is preferable to write this:
1 let result = booksRepository.getAll()
Using the repository pattern you can easily mediate between the domain and the data layer while the unit of work pattern coordinates the write transactions to Core Data.
As previously mentioned, you will want to remove code duplication and to do that you have to create a generic repository that can perform the same operations on any NSManagedObject subclass. Start by defining a protocol to see how that repository will look like.
1 protocol Repository {
2 /// The entity managed by the repository.
3 associatedtype Entity
4
5 /// Gets an array of entities.
6 /// - Parameters:
7 /// - predicate: The predicate to be used for fetching the entities.
8 /// - sortDescriptors: The sort descriptors used for sorting the returned array of entities.
9 func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error>
10
11 /// Creates an entity.
12 func create() -> Result
13
14 /// Deletes an entity.
15 /// - Parameter entity: The entity to be deleted.
16 func delete(entity: Entity) -> Result
17 }
The classes implementing this protocol, regardless if they are working with Core Data, UserDefaults, and 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.
Lastly, 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 deletion was successful.
Now, let's write a specific implementation of how a generic Core Data repository:
1 /// Enum for CoreData related errors
2 enum CoreDataError: Error {
3 case invalidManagedObjectType
4 }
5
6 /// Generic class for handling NSManagedObject subclasses.
7 class CoreDataRepository: Repository {
8 typealias Entity = T
9
10 /// The NSManagedObjectContext instance to be used for performing the operations.
11 private let managedObjectContext: NSManagedObjectContext
12
13 /// Designated initializer.
14 /// - Parameter managedObjectContext: The NSManagedObjectContext instance to be used for performing the operations.
15 init(managedObjectContext: NSManagedObjectContext) {
16 self.managedObjectContext = managedObjectContext
17 }
18
19 /// Gets an array of NSManagedObject entities.
20 /// - Parameters:
21 /// - predicate: The predicate to be used for fetching the entities.
22 /// - sortDescriptors: The sort descriptors used for sorting the returned array of entities.
23 /// - Returns: A result consisting of either an array of NSManagedObject entities or an Error.
24 func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error> {
25 // Create a fetch request for the associated NSManagedObjectContext type.
26 let fetchRequest = Entity.fetchRequest()
27 fetchRequest.predicate = predicate
28 fetchRequest.sortDescriptors = sortDescriptors
29 do {
30 // Perform the fetch request
31 if let fetchResults = try managedObjectContext.fetch(fetchRequest) as? [Entity] {
32 return .success(fetchResults)
33 } else {
34 return .failure(CoreDataError.invalidManagedObjectType)
35 }
36 } catch {
37 return .failure(error)
38 }
39 }
40
41 /// Creates a NSManagedObject entity.
42 /// - Returns: A result consisting of either a NSManagedObject entity or an Error.
43 func create() -> Result {
44 let className = String(describing: Entity.self)
45 guard let managedObject = NSEntityDescription.insertNewObject(forEntityName: className, into: managedObjectContext) as? Entity else {
46 return .failure(CoreDataError.invalidManagedObjectType)
47 }
48 return .success(managedObject)
49 }
50
51 /// Deletes a NSManagedObject entity.
52 /// - Parameter entity: The NSManagedObject to be deleted.
53 /// - Returns: A result consisting of either a Bool set to true or an Error.
54 func delete(entity: Entity) -> Result {
55 managedObjectContext.delete(entity)
56 return .success(true)
57 }
58 }
Writing this generic class, we can now create repositories that handle NSManagedObjects like this:
1 let bookRepository = CoreDataRepository(managedObjectContext: context)
2 let bookResult = bookRepository.create()
3 switch result {
4 case .success(let bookMO):
5 bookMO.title = "The Swift Handbook"
6 case .failure(let error):
7 fatalError("Failed to create book: \(error)")
8 }
9 context.save()
Many times 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 will 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.
Let's build a concrete domain facing repository on top of our existing generic one that handles domain objects instead:
1 /// Protocol that describes a book repository.
2 protocol BookRepositoryInterface {
3 // Get a gook using a predicate
4 func getBooks(predicate: NSPredicate?) -> Result<[Book], Error>
5 // Creates a book on the persistance layer.
6 func create(book: Book) -> Result
7 }
8
9 // Book Repository class.
10 class BookRepository {
11 // The Core Data book repository.
12 private let repository: CoreDataRepository
13
14 /// Designated initializer
15 /// - Parameter context: The context used for storing and quering Core Data.
16 init(context: NSManagedObjectContext) {
17 self.repository = CoreDataRepository(managedObjectContext: context)
18 }
19 }
20 extension BookRepository: BookRepositoryInterface {
21 // Get a gook using a predicate
22 @discardableResult func getBooks(predicate: NSPredicate?) -> Result<[Book], Error> {
23 let result = repository.get(predicate: predicate, sortDescriptors: nil)
24 switch result {
25 case .success(let booksMO):
26 // Transform the NSManagedObject objects to domain objects
27 let books = booksMO.map { bookMO -> Book in
28 return bookMO.toDomainModel()
29 }
30
31 return .success(books)
32 case .failure(let error):
33 // Return the Core Data error.
34 return .failure(error)
35 }
36 }
37 // Creates a book on the persistance layer.
38 @discardableResult func create(book: Book) -> Result {
39 let result = repository.create()
40 switch result {
41 case .success(let bookMO):
42 // Update the book properties.
43 bookMO.identifier = book.identifier
44 bookMO.title = book.title
45 bookMO.author = book.author
46 return .success(true)
47 case .failure(let error):
48 // Return the Core Data error.
49 return .failure(error)
50 }
51 }
52 }
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:
1 struct Book {
2 let identifier: String
3 let title: String
4 let author: String
5 }
While our BookMO class is our persistence facing model class:
1 import Foundation
2 import CoreData
3 @objc(BookMO)
4 public class BookMO: NSManagedObject {
5 }
6 extension BookMO {
7 @nonobjc public class func fetchRequest() -> NSFetchRequest {
8 return NSFetchRequest(entityName: "BookMO")
9 }
10 @NSManaged public var identifier: String?
11 @NSManaged public var title: String?
12 @NSManaged public var author: String?
13 }
To help with the transform from a persistence model to a domain model, you can define a generic protocol to help us with that:
1 protocol DomainModel {
2 associatedtype DomainModelType
3 func toDomainModel() -> DomainModelType
4 }
And for the BookMO class to implement this to facilitate the creation of a Book model you'll have to do the following:
1 extension BookMO: DomainModel {
2 func toDomainModel() -> Book {
3 return Book(identifier: identifier,
4 title: title,
5 author: author)
6 }
7 }
Now we can easily persist a user's favorite book into Core Data by writing the following line:
1 let book = Book(...)
2 bookRepository.create(book: book)
The unit of work pattern will help you keep track of everything that needs to be done in a single save transaction. For example, after creating a book, you might want to add it to a library but if the library is no longer present you want to revert everything.
Another practical aspect of a unit of work is that it can help you with unit testing our implementation in-memory context for example.
1 class UnitOfWork {
2 /// The NSManagedObjectContext instance to be used for performing the unit of work.
3 private let context: NSManagedObjectContext
4
5 /// Book repository instance.
6 let bookRepository: BookRepository
7
8 /// Designated initializer.
9 /// - Parameter managedObjectContext: The NSManagedObjectContext instance to be used for performing the unit of work.
10 init(context: NSManagedObjectContext) {
11 self.context = context
12 self.bookRepository = BookRepository(context: context)
13 }
14
15 /// Save the NSManagedObjectContext.
16 @discardableResult func saveChanges() -> Result {
17 do {
18 try context.save()
19 return .success(true)
20 } catch {
21 context.rollback()
22 return .failure(error)
23 }
24 }
25 }
In order to use the unit of work we will have to do the following:
1 let unitOfWork = UnitOfWork(context: newBackgroundContext)
2 unitOfWork.bookRepository.create(book: book)
3 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.
Hope this provides the inside scoop you needed and if you have any questions don't hesitate to contact us.
More articles from Ciprian Redinciuc can be found on Userdesk or OceanoBe blog.
Follow for more interesting information with Continuous delivery for iOS applications using Jenkins and Fastlane or Swift networking with Combine.