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:
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 }()</person></person>
Here we create persistentContainer
and fetchedResultsController
objects, which will be responsible for the filling
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 controllerdo{ 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.
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 {returnfalse} 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)}}} </nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>
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()breakcase .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()breakcase .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)inif let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)")}})return container }() </car></car>
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 })}} </car></car></car>
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. 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)inif 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)")} </t:></t:></t:>
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 returnNSEntityDescription.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 }} </nsmanagedobject></nsmanagedobject></t:></t:></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>
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.