Swift — 4 — Core Data — Part 3 Creating a Singleton Core Data & refactoring insert, update, delete operations

Alok Upadhyay
14 min readMar 29, 2018

--

Hello guys :D . In part 2 we learned about basic core data concepts with insert, update and delete operations. Before starting unit testing for core data we have to loosely couple our code. It will eventually make our view controller class also light weight. Download Part 2 code for Starter.

In this part we will separate the core data related business logic in a separate class. Final source code is at end of this tutorial.

Have a ☕ and start code –

Add a file in your project and name it “CoreDataManager”.

Copy below code and paste it to “CoreDataManager”.

import Foundation
import CoreData
import UIKit
class CoreDataManager {

//1
static let sharedManager = CoreDataManager()
//2.
private init() {} // Prevent clients from creating another instance.

//3
lazy var persistentContainer: NSPersistentContainer = {

let container = NSPersistentContainer(name: "PersonData")


container.loadPersistentStores(completionHandler: { (storeDescription, error) in

if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

//4
func saveContext () {
let context = CoreDataManager.sharedManager.persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

Let’s understand what’s going on: :roll:

  1. We are creating a static let, so that sharedManager have same instance and can not be changed.
  2. Using private keyword, so that this class can not be initialize mistakenly. If at some place you will try to initialize it again you will get compile time error.
  3. Initializing NSPersistentContainer, thus initializing complete core data stack lazily. persistentContainer object will be initialized only when it is needed. I have already over detailed in part 1.
  4. Save context method will save our uncommitted changes in core data store. I have already over detailed in part 1.

As we have put core data related code in separate file CoreDataManager, we will remove all code related to core data from AppDelegate file.

  1. Remove import CoreData
  2. Remove lazy var persistentContainer: NSPersistentContainer = {……………………}()
  3. change applicationWillTerminate as below:
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
CoreDataManager.sharedManager.saveContext()
}

Your AppDelegate file should look like this:

import UIKit@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
CoreDataManager.sharedManager.saveContext()
}
}

See how much slim AppDelegate file looking now 8-)

Run your project and see errors :x

We are getting these error because we changed our AppDelegate file and we have still not make changes in ViewController class.

Open ViewController class and search for

appDelegate.persistentContainer.viewContext

replace appDelegate.persistentContainer.viewContext with

CoreDataManager.sharedManager.persistentContainer.viewContext

Build and run, it should work now. :D

We are still getting some warning in ViewController, so we need to remove code related to appDelegate.

Search & Delete wherever you find below code

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}

Build and run your code it should work fine now.

Now moving ahead we need to write insert, update, delete and fetch methods in CoreDataManager class. That is we are going to refactor ViewController class.

Insert

copy code from ViewController and paste it in CoreDataManagerClass.

func save(name: String, ssn : Int16) {

/*1.
Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
Think of saving a new managed object to Core Data as a two-step process: first, you insert a new managed object into a managed object context; then, after you’re happy with your shiny new managed object, you “commit” the changes in your managed object context to save it to disk.
Xcode has already generated a managed object context as part of the new project’s template. Remember, this only happens if you check the Use Core Data checkbox at the beginning. This default managed object context lives as a property of the NSPersistentContainer in the application delegate. To access it, you first get a reference to the app delegate.
*/
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

/*
An NSEntityDescription object is associated with a specific class instance
Class
NSEntityDescription
A description of an entity in Core Data.

Retrieving an Entity with a Given Name here person
*/
let entity = NSEntityDescription.entity(forEntityName: "Person",
in: managedContext)!


/*
Initializes a managed object and inserts it into the specified managed object context.

init(entity: NSEntityDescription,
insertInto context: NSManagedObjectContext?)
*/
let person = NSManagedObject(entity: entity,
insertInto: managedContext)

/*
With an NSManagedObject in hand, you set the name attribute using key-value coding. You must spell the KVC key (name in this case) exactly as it appears in your Data Model
*/
person.setValue(name, forKeyPath: "name")
person.setValue(ssn, forKeyPath: "ssn")

/*
You commit your changes to person and save to disk by calling save on the managed object context. Note save can throw an error, which is why you call it using the try keyword within a do-catch block. Finally, insert the new managed object into the people array so it shows up when the table view reloads.
*/
do {
try managedContext.save()
people.append(person)
tableView.reloadData()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}

We do not need tableView, people.append..blah blah code.

We will make minor changes in above code and below is the fixed version.

func insertPerson(name: String, ssn : Int16)->Person? {

/*1.
Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
Think of saving a new managed object to Core Data as a two-step process: first, you insert a new managed object into a managed object context; then, after you’re happy with your shiny new managed object, you “commit” the changes in your managed object context to save it to disk.
Xcode has already generated a managed object context as part of the new project’s template. Remember, this only happens if you check the Use Core Data checkbox at the beginning. This default managed object context lives as a property of the NSPersistentContainer in the application delegate. To access it, you first get a reference to the app delegate.
*/
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

/*
An NSEntityDescription object is associated with a specific class instance
Class
NSEntityDescription
A description of an entity in Core Data.

Retrieving an Entity with a Given Name here person
*/
let entity = NSEntityDescription.entity(forEntityName: "Person",
in: managedContext)!


/*
Initializes a managed object and inserts it into the specified managed object context.

init(entity: NSEntityDescription,
insertInto context: NSManagedObjectContext?)
*/
let person = NSManagedObject(entity: entity,
insertInto: managedContext)

/*
With an NSManagedObject in hand, you set the name attribute using key-value coding. You must spell the KVC key (name in this case) exactly as it appears in your Data Model
*/
person.setValue(name, forKeyPath: "name")
person.setValue(ssn, forKeyPath: "ssn")

/*
You commit your changes to person and save to disk by calling save on the managed object context. Note save can throw an error, which is why you call it using the try keyword within a do-catch block. Finally, insert the new managed object into the people array so it shows up when the table view reloads.
*/
do {
try managedContext.save()
return person as? Person
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
return nil
}
}

Now open ViewController class and change

func save(name: String, ssn : Int16)

as per below code:

func save(name: String, ssn : Int16) {
//1
let person = CoreDataManager.sharedManager.insertPerson(name: name, ssn: ssn)
//2
if person != nil {
people.append(person!)//3
tableView.reloadData()//4
}
}

See the beauty of code now. It’s very light weight. isn’t it :mrgreen:

  1. We are calling insertPerson method of CoreDataManager class, it will return a optional person :?: object.
  2. Before unwrapping it we must make sure it is not nil to be saved from run time crash. If it is not nil then only force unwrap it. You can also use optional binding to get unwrapped value.
  3. Add this object in table view data source array
  4. Reload table finally.

Let’s run it and see everything is working fine. Yes all good till now.

Let’s refactor code of updation.

Update

In ViewController search for

func update(name:String, ssn : Int16, person : Person)

method. We will copy this method as it is and paste it in CoreDataManager class.

func update(name:String, ssn : Int16, person : Person) {

/*1.
Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
Think of saving a new managed object to Core Data as a two-step process: first, you insert a new managed object into a managed object context; then, after you’re happy with your shiny new managed object, you “commit” the changes in your managed object context to save it to disk.
Xcode has already generated a managed object context as part of the new project’s template. Remember, this only happens if you check the Use Core Data checkbox at the beginning. This default managed object context lives as a property of the NSPersistentContainer in the application delegate. To access it, you first get a reference to the app delegate.
*/
let context = CoreDataManager.sharedManager.persistentContainer.viewContext

do {


/*
With an NSManagedObject in hand, you set the name attribute using key-value coding. You must spell the KVC key (name in this case) exactly as it appears in your Data Model
*/
person.setValue(name, forKey: "name")
person.setValue(ssn, forKey: "ssn")

print("\(person.value(forKey: "name"))")
print("\(person.value(forKey: "ssn"))")

/*
You commit your changes to person and save to disk by calling save on the managed object context. Note save can throw an error, which is why you call it using the try keyword within a do-catch block. Finally, insert the new managed object into the people array so it shows up when the table view reloads.
*/
do {
try context.save()
print("saved!")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {

}

} catch {
print("Error with request: \(error)")
}
}

Now open ViewController file and change implementation of

func update(name:String, ssn : Int16, person : Person)

method to as per below implementation:

func update(name:String, ssn : Int16, person : Person) {
CoreDataManager.sharedManager.update(name: name, ssn: ssn, person: person)
}

Look at the code man! We shredded a lot of weight from this method. Cheers :)

Build it, insert some data and update it, it should work.

Now lets change delete implementation and make delete available in CoreDataManager class. Copy complete delete(person:) function with it’s implementation from ViewController and paste in CoreDataManager class.

func delete(person : Person){


let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

do {

managedContext.delete(person)

} catch {
// Do something in response to error condition
print(error)
}

do {
try managedContext.save()
} catch {
// Do something in response to error condition
}
}

Explanation pretty simple. Get managed object context. Call delete method and pass person object as parameter. Finally call do-try-catch to save uncommitted changes of managedContext.

Now change

func delete(person : Person)

implementation as below:

func delete(person : Person){
CoreDataManager.sharedManager.delete(person: person)
}

Are you looking at this code 8-) . So light weight. Let’s again build, run and test insert, update and delete code, everything should work fine.

Finally lets quickly refactor code of fetchAllPersons() and then func delete(ssn: String).

Open CoreDataManager class and paste below code:

func fetchAllPersons(){


/*Before you can do anything with Core Data, you need a managed object context. */
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

/*As the name suggests, NSFetchRequest is the class responsible for fetching from Core Data.

Initializing a fetch request with init(entityName:), fetches all objects of a particular entity. This is what you do here to fetch all Person entities.
*/
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

/*You hand the fetch request over to the managed object context to do the heavy lifting. fetch(_:) returns an array of managed objects meeting the criteria specified by the fetch request.*/
do {
people = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}

}

Make minor changes in above code:

func fetchAllPersons() -> [Person]?{


/*Before you can do anything with Core Data, you need a managed object context. */
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

/*As the name suggests, NSFetchRequest is the class responsible for fetching from Core Data.

Initializing a fetch request with init(entityName:), fetches all objects of a particular entity. This is what you do here to fetch all Person entities.
*/
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

/*You hand the fetch request over to the managed object context to do the heavy lifting. fetch(_:) returns an array of managed objects meeting the criteria specified by the fetch request.*/
do {
let people = try managedContext.fetch(fetchRequest)
return people as? [Person]
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return nil
}

}

Now we will refactor code for delete function, which uses predicate.

Paste below method in CoreDataManager class.

func delete(ssn: String) -> [Person]? {
/*get reference to appdelegate file*/


/*get reference of managed object context*/
let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

/*init fetch request*/
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

/*pass your condition with NSPredicate. We only want to delete those records which match our condition*/
fetchRequest.predicate = NSPredicate(format: "ssn == %@" ,ssn)
do {

/*managedContext.fetch(fetchRequest) will return array of person objects [personObjects]*/
let item = try managedContext.fetch(fetchRequest)
var arrRemovedPeople = [Person]()
for i in item {

/*call delete method(aManagedObjectInstance)*/
/*here i is managed object instance*/
managedContext.delete(i)

/*finally save the contexts*/
try managedContext.save()

/*update your array also*/
arrRemovedPeople.append(i as! Person)
}
return arrRemovedPeople

} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return nil
}

}

change implementation of

delete(ssn: String)

in ViewController class as below:

func delete(ssn: String) {

let arrRemovedObjects = CoreDataManager.sharedManager.delete(ssn: ssn)
people = people.filter({ (param) -> Bool in

if (arrRemovedObjects?.contains(param as! Person))!{
return false
}else{
return true
}
})

}

Explanation — We are using swift high order function filter, to remove objects of arrRemovedObjects from people array.

var array1 = ["a", "b", "c", "d", "e"]
let array2 = ["a", "c", "d"]
array1 = array1.filter { !array2.contains($0) }//output [b,e]

You can also create your own logic using for loop ;) .

Let’s run our code and test all operations. Every thing is working perfect.

:D 8-) ;) .

I write here also http://iosdose.com/wp/2018/03/29/swift-4-core-data-part-3-creating-a-singleton-coredatamanager-class-refactoring-insert-update-delete-operation/

As promised Sourcecode is here.

Below is the link to all tutorials.

Bluetooth low energy (BLE) integration with Flutter.

https://iosdose.com/ble-operations-in-flutter/

Flutter Google Map, Current Location, Marker, Polyline & Autocomplete

Flutter Google Map, Current Location & Marker

RxSwift & MVVM design pattern

https://iosdose.com/gorgeous-rxswift-binding-and-mvvm-design-pattern/

SwiftUI based News App

VIPER

Routing/presenting in VIPER architecture

Protocol Delegate Design Pattern

https://iosdose.com/creating-an-ios-app-using-protocol-delegate-design-pattern/

MVVM using Swift

Optionals & 6 ways to unwrap

Multiline UILabel

StackView

Instrumentation (Leak, Allocation)

BLE Using Swift

Swift Interview Questions & Answers

Coredata Basics

Simple Core Data App Using UITableView

Creating Singleton Core Data Class

https://iosdose.com/swift-4-core-data-part-3-creating-a-singleton-coredatamanager-class-refactoring-insert-update-delete-operation/

Core Data Test Cases

Website: https://tinyurl.com/y5m695rf

--

--