Hello CoreData!

Today we will review framework under the terrible name CoreData, from Apple. I hate it so much. This is Apple's solution to work with SQLite (a relational database). CoreData can store Swift objects in SQLite, and also it can perform the reverse operation.

Introduction

I personally prefer the Realm, since you don’t need to do a lot of movements to make it work: simply install and begin to create all what you want, plus the official website has a great documentation for all features. But when you look through the vacancies of other companies (I personally never do it. Other companies... they exist? For real?!:)). Often you see, that CoreData is required, and if you do not have experience with this framework, then you can easily became «gone with the wind» at the beginning of the interview, and nobody well ever know all about those beautiful buttons you can do :).

So why do I don’t like this framework? Because it is heavy for the understanding for the beginners, and it is easy to get confused and make mistakes, and the more code you have written, the more likely that somewhere you could be wrong.

Starting with ios 10, Apple have done much to facilitate the work with CoreData. For example, they've presented NSPersistentContainer object, that takes over the task of creating NSManagedObjectModel, NSPersistentStoreCoordinator and NSManagedObjectContext. Now for getting started you just need to to initialize the persistentContainer = NSPersistentContainer(name: "MyModel") object. At the beginning this article was intended as a review of third-party solutions for the CoreData, but since Apple has altered this framework, many of those solutions were either in the remaking process or worse — they were written in Objective-C. So we will have three examples. In the first one we’ll go through NSFetchedResultsController and NSPersistentContainer; in the second, we will begin to write the easy-to-use class, and in the last example we'll improve the class by adding generics. .

Example 1

The project is available for study and called MagicalRecord_project (originally I wanted to use this framework). Let's view the Model.xcdatamodeld file:

Viewing Model.xcdatamodeld

Viewing Model.xcdatamodeld

For avatar attribute (of Binary Data type) an option Allows External Storage was turned on..

Let's move to the controller:

fileprivate let persistentContainer: NSPersistentContainer = NSPersistentContainer(name: "Model")
 
    fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Person> = {
        let fetchRequest = NSFetchRequest<Person>()
        fetchRequest.entity = Person.entity()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        let frc = NSFetchedResultsController(fetchRequest: fetchRequest,
                                             managedObjectContext: self.persistentContainer.viewContext,
                                             sectionNameKeyPath: "name",
                                             cacheName: nil)
        frc.delegate = self
        return frc
    }()

Here we create persistentContainer and fetchedResultsController objects, which will be responsible for the filling with the data. Also, we'll create a method that will help us to create objects:

func createPersonObject(name: String, about: String, image: UIImage) {
        let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: persistentContainer.viewContext) as! Person
        person.name = name
        person.about = about
        person.avatar = UIImageJPEGRepresentation(image, 1.0) as NSData?
    }

Watch how it all looks in viewDidLoad:

override func viewDidLoad() {
        super.viewDidLoad()
 
        tableView.delegate = self
        tableView.dataSource = self
 
        //create data
        createPersonObject(name: "Name A", about: "some inform...", image: UIImage(named: "01")!)
        createPersonObject(name: "Name B", about: "some inform...", image: UIImage(named: "02")!)
 
        perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0)
 
        //fetch data in controller
        do {
            try fetchedResultsController.performFetch()
        } catch {
            print(error)
        }
    }

This line perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0) adds a third object to the context, and NSFetchedResultsController updates tableView (we'll talk about it later).

fetchedResultsController.performFetch() transmits data from CoreData to the controller.

Now run this app.

Emulator

Emulator after adding new object

At the beginning we see two added object, and after two seconds — three. Let's create an extension for ViewController:

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return false
    }
 
    func numberOfSections(in tableView: UITableView) -> Int {
        return fetchedResultsController.sections?.count ?? 0
    }
 
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController.sections?[section].numberOfObjects ?? 0
    }
 
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
 
        guard let sections = fetchedResultsController.sections else {
            fatalError("Sections missing")
        }
 
        let section = sections[indexPath.section]
        guard let itemsInSection = section.objects as? [Person] else {
            fatalError("Items missing")
        }
 
        let person = itemsInSection[indexPath.row]
 
        cell.aboutLbl.text = person.about
        cell.nameLbl.text = person.name
        cell.avatarImView.image = UIImage(data: person.avatar! as Data)
 
        return cell
    }
}

Since we have fetchedResultsController instead of dataSource array, we use it to obtain all the necessary data. The variable fileprivate lazy var fetchedResultsController we've declared ourselves a delegate of NSFetchedResultsController. Now we gonna make the necessary methods:

extension ViewController: NSFetchedResultsControllerDelegate {
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }
 
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
 
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
 
        switch type {
        case .insert: tableView.insertRows(at: [newIndexPath!], with: .automatic)
        case .delete: tableView.deleteRows(at: [indexPath!], with: .automatic)
        case .update: tableView.reloadRows(at: [indexPath!], with: .automatic)
        case .move: tableView.moveRow(at: indexPath!, to: newIndexPath!)
        }
    }
 
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
 
        switch type {
        case .insert: tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)
        case .delete: tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)
        case .move, .update: tableView.reloadSections(IndexSet(integer: sectionIndex), with: .automatic)
        }
    }
}

Now, when something changes (by adding, updating, or deleting some element), NSFetchedResultsController knows about it and can make the necessary corrections in UITableView. Such kind of mechanism is implemented in the Realm:

let token = realm.addNotificationBlock { notification, realm in
    viewController.updateUI()
}
 
token = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
      guard let tableView = self?.tableView else { return }
      switch changes {
      case .initial:
        // Results are now populated and can be accessed without blocking the UI
        tableView.reloadData()
        break
      case .update(_, let deletions, let insertions, let modifications):
        // Query results have changed, so apply them to the UITableView
        tableView.beginUpdates()
        tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                           with: .automatic)
        tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                           with: .automatic)
        tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 
                           with: .automatic)
        tableView.endUpdates()
        break
      case .error(let error):
        // An error occurred while opening the Realm file on the background worker thread
        fatalError("\(error)")
        break
      }
    }
  }

It ain't suits with our example, but as you can see, the UITableView update mechanisms are similar.

Example 2

In this example we will create StoreManager. Open StoreManager_project project. StoreManager was created using Singleton pattern (one copy for the life of the program):

fileprivate override init(){}
    static let sharedInstance = StoreManager()
    override func copy() -> Any {
        fatalError("You are not allowed to use copy method on singleton!")
    }
 
fileprivate lazy var fetchRequest: NSFetchRequest<Car> = {
        let request = NSFetchRequest<Car>()
        request.entity = Car.entity()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        return request
    }()
 
    fileprivate lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Model")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

NSPersistentContainer has two properties, more specifically, the property and method: viewContext and newBackgroundContext. The first is associated with the main queue, the second with privateQueueConcurrencyType. When we write something in newBackgroundContext, it sends a notification for viewContext object to merge content. Therefore, it appears that we no longer need to subscribe to this notification. From viewContext documentation:

This context is configured to be generational and to automatically consume save notifications from other contexts.

Due to newBackgroundContext:

…is set to consume NSManagedObjectContextDidSave broadcasts automatically.

I've added a few methods to modify in third example:

func saveMainContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
 
    func clearMainStorage() {
        var cars: [Car]?
        do {
            cars = try persistentContainer.viewContext.fetch(fetchRequest)
        } catch {
            print("Storage reading error!")
        }
        if let cars = cars {
            for item in cars {
                persistentContainer.viewContext.delete(item)
            }
        }
    }
 
    func addACar(name: String, hasRoof: Bool, numberOfWheels: Int) {
        //add data to background context
        let context = persistentContainer.newBackgroundContext()
        let car = NSEntityDescription.insertNewObject(forEntityName: "Car", into: context) as! Car
        car.name = name
        car.numberOfWheels = Int16(numberOfWheels)
        car.hasRoof = hasRoof
        do {
            try context.save()
        } catch {
            print(error)
        }
    }
 
    func showMainStorage() {
        var cars: [Car]?
        do {
            cars = try persistentContainer.viewContext.fetch(fetchRequest)
        } catch {
            print("Storage reading error!")
        }
        print("CARS: \(cars?.map { $0.name! })")
    }
 
viewDidLoad из ViewController:
 
StoreManager.sharedInstance.clearMainStorage()
 
        StoreManager.sharedInstance.addACar(name: "BMW", hasRoof: true, numberOfWheels: 4)
        StoreManager.sharedInstance.addACar(name: "Zapor", hasRoof: false, numberOfWheels: 3)
        StoreManager.sharedInstance.addACar(name: "Fiat", hasRoof: true, numberOfWheels: 5)
        StoreManager.sharedInstance.addACar(name: "F1", hasRoof: false, numberOfWheels: 4)
 
        perform(#selector(insertAnotherCar), with: nil, afterDelay: 2.0)
        StoreManager.sharedInstance.showMainStorage()

First, we clean the storage and then add the car models. Then there is a method to add a third car, after two seconds (never use performSelector: afterDelay: — it is a bad practice). And two methods showMainStorage(), which shows how the database has changed. For example, you can uncomment the following piece of code:

NotificationCenter.default.addObserver(self, selector: #selector(storageHasChanged(note:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
 
deinit {
        NotificationCenter.default.removeObserver(self)
    }
 
//MARK: Actions
    func storageHasChanged(note: NSNotification) {
        let inserted = note.userInfo?["inserted"]
        let deleted = note.userInfo?["deleted"]
        let updated = note.userInfo?["updated"]
 
        if let insertedSet = inserted as? Set<Car>, insertedSet.count > 0 {
            print("INSERTED")
            print(insertedSet.map { $0.name })
        }
        if let deletedSet = deleted as? Set<Car>, deletedSet.count > 0 {
            print("DELETED")
            print(deletedSet.map { $0.name })
        }
        if let updatedSet = updated as? Set<Car>, updatedSet.count > 0 {
            print("UPDATED")
            print(updatedSet.map { $0.name })
        }
    }

You can experiment with the notification of the database change. My result:

[Optional("Zapor")]
INSERTED
[Optional("Fiat")]
INSERTED
[Optional("F1")]
DELETED
[Optional("Zapor"), Optional("F1"), Optional("Fiat"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("Alfa Romeo"), Optional("Alfa Romeo"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("F1"), Optional("F1"), Optional("Alfa Romeo"), Optional("F1"), Optional("Fiat"), Optional("Alfa Romeo"), Optional("Zapor"), Optional("Zapor"), Optional("Zapor")]
CARS: Optional(["BMW", "F1", "Fiat", "Zapor"])
INSERTED
[Optional("Alfa Romeo")]
CARS: Optional(["Alfa Romeo", "BMW", "F1", "Fiat", “Zapor"])

Example 3

In this example, I wanted to demonstrate how to work with JSON. Of course, in a real project I would use something for mapping, SwiftyJSON for simplicity, maybe. But for a little lesson, I requested JSON as a string and then parsed it:

et json = "{\"cars\":[{\"name\":\"ZaZ\"},{\"name\":\"Lada\"},{\"name\":\"Lexus\"}]}"
        let dict = try! JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.allowFragments) as! [String:Any]

And in the StoreManager file:

//to make sure we have only one copy
    lazy var backgroundContext: NSManagedObjectContext = {
        return self.persistentContainer.newBackgroundContext()
    }()
 
    fileprivate lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Model")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()
 
    func saveContext(type: ContextType) {
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
 
    func getMainContextObjects<T: NSManagedObject>(entity: T.Type) -> [NSManagedObject]? {
        return persistentContainer.viewContext.fetchAll(entity: entity, fetchConfiguration: nil)
    }
 
    func clearStorage<T: NSManagedObject>(type: ContextType, entity: T.Type) {
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        let objects = context.fetchAll(entity: entity, fetchConfiguration: nil)
        if let objects = objects {
            for item in objects {
                context.delete(item)
            }
        }
    }
 
    func showStorage<T: NSManagedObject>(type: ContextType, entity: T.Type) {
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        let objects = context.fetchAll(entity: entity, fetchConfiguration: nil)
        print("OBJECTS: \(objects)")
        print("COUNT: \(objects?.count)")
    }

Here I keep backgroundContext as a variable to always get the only one backgroundContext, which afterwards I can save by saveContext(type:) method. Other methods are clear to understand. Since we have an enum...

enum ContextType {
    case main
    case background
}

...we always know the context we're working with. Let's go back to the ViewController. Notice how I create objects:

for item in dict["cars"] as! [Any] {
            let car = StoreManager.sharedInstance.backgroundContext.insert(entity: Car.self)
            car.name = (item as? [String:Any])?["name"] as! String?
        }

But there's no context in insert(entity:) method! The thing is, that I wrote extensions to work with generic data.

//MARK: CoreData extensions
extension NSManagedObject {
    class var entityName : String {
        let components = NSStringFromClass(self)
        return components
    }
 
    class func fetchRequestObj() -> NSFetchRequest<NSFetchRequestResult> {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: self.entityName)
        return request
    }
 
    class func fetchRequestWithKey(key: String, ascending: Bool = true) -> NSFetchRequest<NSFetchRequestResult> {
        let request = fetchRequestObj()
        request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
        return request
    }
}
 
extension NSManagedObjectContext {
    func insert<T: NSManagedObject>(entity: T.Type) -> T {
        let entityName = entity.entityName
        return NSEntityDescription.insertNewObject(forEntityName: entityName, into: self) as! T
    }
 
    func fetchAll<T: NSManagedObject>(entity: T.Type, fetchConfiguration: ((NSFetchRequest<NSManagedObject>) -> Void)?) -> [NSManagedObject]? {
        let dataFetchRequest = NSFetchRequest<NSManagedObject>(entityName: entity.entityName)
 
        fetchConfiguration?(dataFetchRequest)
        var result = [NSManagedObject]()
        do {
            result = try self.fetch(dataFetchRequest)
        } catch {
            print("Failed to fetch feed data, critical error: \(error)")
        }
        return result
    }
}

So you can add entity in context using its type and self (Car.self). Also the fetchAll method to work with generics war added. We use it in StoreManager methods. Now our manager is ready to go. I hope to test and improve it if needed.

Related posts

Return to list Return to list