Sync PhoneBook / Contacts with Core Data in iOS Swift

Author - Vishal Dodiya

When you are working with contact book and save contact in Core Data(Local Database), you need to sync contact everytime when contact book updates. Which means to update the phone number, name, delete the contact, create new contact etc.

Sometimes when we have a bunch of contacts in our contact book and try to store it in Core Data we face many issues like Threading issue, UI stuck issue, Store data in background issue, etc.

Here you can find the easiest way to sync contacts with Core Data,  you can easily get the array of deleted, updated and newly added contact list.

Project Setup

1) Open Xcode and create new project based on single view template.

2) Make sure to set Swift as a Language and “Use Core Data” is checked

After successful setup the project we are going to create a Core Data manager class.

Create Core data Helper class

Create a new swift file and give it a name “CoredataManager.swift”. This class helps to perform all the Core Data operation in a single class.

In this class, we write operations based on Core Data to store and update contact data.

After creating CoreDataManager.Swift file we are going to implement logic for managing Core Data stack.

1) Import Core Data  & Create Class

import Foundation
import CoreData

class CoreDataManager {

}

2) Create instance of the CoreDataManager & declare static name of the database for future use.

static let DBName = "ContactSyncBGDemo"
static let sharedInstance = CoreDataManager()

3) Setting the Core Data stack

A Persistent coordinate encapsulates the core-data stack in your application. To set up the core-data stack, we need to instantiate the following object.

I) Manage Object Model
ii) Manage Object Context
iii) Persistent Store Coordinator

Managed Object Model

Models describe object graphs to be managed. Model are editable until they are used by a persistent store coordinator, allow developers to create/modify them dynamically. However, once a model is being used, then it must not be changed.

We initialize managed object model like below:-

private lazy var managedObjectModel: NSManagedObjectModel = {
        let modelURL = Bundle.main.url(forResource: CoreDataManager.DBName, withExtension: "momd")!
        return NSManagedObjectModel(contentsOf: modelURL)!
}()

Managed Object Context

A managed object context is a scratch pad for your managed objects. Manage object context responsibility is to manage the collection of managed objects.  These managed objects represent an internally consistent view of one or more persistent store.

Most of the application needs just a single context. It is default configuration in most of core data application and it is associated with the main queue.

But in some situation, like you have much data and it takes a long time to manage, will block the main thread of the application then it is helpful to treat managed object context as a set of changes and application can discard when it is not needed. It will be possible using child context.

Here we initialize two managed object context one is for the main thread and another is for background thread.

lazy var managedObjectContext: NSManagedObjectContext = 
{
    var managedObjectContext: NSManagedObjectContext?
    managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    managedObjectContext?.parent = self.bGManagedObjectContext
    managedObjectContext?.automaticallyMergesChangesFromParent = true
    return managedObjectContext!
}()
lazy var bGManagedObjectContext: NSManagedObjectContext = 
{
    let taskContext = self.persistentContainer.newBackgroundContext()
    taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    taskContext.undoManager = nil
    return taskContext
}()

The below code creates NSPersistentContainer object.

lazy var persistentContainer: NSPersistentContainer = 
{
    let container = NSPersistentContainer(name: CoreDataManager.DBName)
    container.loadPersistentStores(completionHandler: 
    { (storeDescription, error) in
        if let error = error as NSError? 
        {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    container.viewContext.undoManager = nil
    container.viewContext.shouldDeleteInaccessibleFaults = true
    container.viewContext.automaticallyMergesChangesFromParent = true
    return container
}()

Persistent Store Coordinator

Core Data persistent store coordinator are actually the instance of the NSPersistentStoreCoordinator class, It has a reference of a managed object model that describe the entities in the store.

The persistent store coordinator is the middleware in the core data stack.

The Core Data is only working once the persistent store is added to the persistent store coordinator.

We can instantiate an instance of persistent store coordinator class using the manage object model.

This is what the implementation of the persistentStoreCoordinator instance looks like: –

private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = 
{
    let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
    let url = self.applicationDocumentsDirectory.appendingPathComponent("\(CoreDataManager.DBName).sqlite")
    var failureReason = "There was an error creating or loading the application's saved data."
    do 
    {
        // Configure automatic migration.
        let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
        try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options)
    }
    catch 
    {
	// Report any error we got.
        var dict = [String: AnyObject]()
        dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
        dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?
        dict[NSUnderlyingErrorKey] = error as NSError
        let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
        NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
        abort()
    }
    return coordinator
}

Where the URL is the location of the persistent store. We used the value stored in applicationDocumentDirectory and append the name of the persistent store.

let url = self.applicationDocumentsDirectory.appendingPathComponent("\(CoreDataManager.DBName).sqlite")
//and the applicationDocumentsDirectory is the path of the document directory and append the name and extension of the persistent store. It look like below: 
lazy var applicationDocumentsDirectory: URL = 
{
    let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return urls.last!
}()

Saving Changes

We implement the helper method in the CoreDataManager class to save changes to the managed object context. In this method, we access the managed object context of the persistent container through a viewContext property.

We can also check the context has any changes by using the hasChanges property and we invoke save() method of the managed object context.

The saveChange method looks like below: –

func saveContext() 
{
    managedObjectContext.perform
    {
        do
        {
            if self.managedObjectContext.hasChanges
            {
                try self.managedObjectContext.save()
            }
        }
        catch
        {
            let saveError = error as NSError
            print("Unable to Save Changes of Managed Object Context")
            print("\(saveError), \(saveError.localizedDescription)")
        }            
        self.privateManagedObjectContext.perform 
        {
            do
            {
                if self.privateManagedObjectContext.hasChanges 
                {
                    try self.privateManagedObjectContext.save()
                }
            }
            catch
            {
                let saveError = error as NSError
                print("Unable to Save Changes of Private Managed Object Context")
                print("\(saveError), \(saveError.localizedDescription)")
            }
        }
    }
}

We create one more method for saving changes in background and save using the bGManagedObjectContext. It looks like below: –

func saveContextInBG()
{
    do 
    {
        if self.bGManagedObjectContext.hasChanges
        {
            try self.bGManagedObjectContext.save()
        }    
    }
    catch
    {
        print(error)
    }
}

Setup helper method for the manage CoreData

Now our CoreData stack is ready and we are going to create some method for creating an entity, fetch object, delete the object from core-data stack entity.

1) Create Object or Insert new object

This method is used to insert new object in your entity: –

func createObjectForEntity(entityName:String,taskContext: NSManagedObjectContext) -> AnyObject?
{
    if (entityName != "")
    {
        let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: taskContext)
        let createdObject = NSManagedObject(entity: entityDescription!, insertInto: taskContext)
        return createdObject as AnyObject
    }
    return nil
}

2) Fetch Objects

Data are stored in the persistent store, you will use an NSFetchRequest to access that data. You need to provide an entity name to fetch the object of which entity. It will return the result type of the fetch object.

Here is the helper method to fetch objects with optional parameters.

func getObjectsforEntity(strEntity : String, ShortBy : String, isAscending : Bool, predicate : NSPredicate!, groupBy : NSString, taskContext : NSManagedObjectContext) -> AnyObject 
{
    let fetchRequest = NSFetchRequest<NSFetchRequestResult> (entityName: strEntity)
    fetchRequest.returnsObjectsAsFaults = false
    if predicate != nil 
    {
        fetchRequest.predicate = predicate
    }
    if (ShortBy != "")
    {
        let sortDescriptor1 = NSSortDescriptor(key: ShortBy, ascending: isAscending)
        fetchRequest.sortDescriptors = [sortDescriptor1]
    }
    if groupBy != "" 
    {
        fetchRequest.propertiesToGroupBy = [groupBy]
    }
    do
    {
        let result = try taskContext.fetch(fetchRequest)
        return result as AnyObject
    }
    catch
    {
        let fetchError = error as NSError
        print(fetchError)
        return nil as [AnyObject]? as AnyObject
    }
}

strEntity: strEntity is the entity name of the fetch objects.
isAscending: You need to provide bool value to fetch result in ascending or descending order.
Predicate: This is an optional parameter. You can pass your predicate to filter results.
groupBy: Specifies the way in which data should be grouped before a select statement is run in an SQL database.
taskContext: Here you need to pass object of NSManagedObjectContext to fetch request using that context.

3) Delete Object:

When you want to delete a specific object from the Core Data entity, you can use this method. In this method, you need to pass the object of NSManagedObject which will be deleted from storage and object of NSManagedObjectContext to save changes and used for delete.

The deleteObject method looks like below: –

func deleteObject(object : NSManagedObject,taskContext: NSManagedObjectContext)
{
    taskContext.delete(object)
    if(taskContext == self.bGManagedObjectContext)
    {
        self.saveContextInBG()
    }
    else
    {
        self.saveContext()
    }
}

Now Our CoreDataHelper class is ready and we are going to create ContactUtility class to manage contact sync functionality.

Create ContactUtility class

The ContactUtility.swift class is used to implement contact sync functionality with Core Data. Let’s create and understand helper class to sync contact book.

I) Import libraries  & create class

Create a new swift file and give it a name “ContactUtility.swift”, follow the same steps as we created CoreDataManager class.

Here we will use Contacts library to access contact book, so import contacts and ContactUI framework in the Link Binary with Library of the build Phase section in the project setting.

After importing a library, we need to write import statement in our helper class and declare a class which looks like below: –

import Contacts
import ContactsUI
import CoreData

class ContactUtility: NSObject
{
    
}

II) Declare global variable and create shared Instance of utility class

CNContactStore : CNContactStore is used to fetch and save contacts, groups, and container.

The object of CNContactStore which will be declared below, we will use it for check authorization  and fetch contact.

let contactStore = CNContactStore()
static let sharedInstance:ContactUtility = 
{
    let instance = ContactUtility()
    return instance
}()

III) Sync Contact with helper method:

After declaring variable and class we are going to implement functionality. We will follow some basic steps and sync contact with core data.

Our contact sync flow steps are below: –

Step 1

First of all, we are going to fetch contact list from our device’s phonebook so we need to get permission for contact fetch.

We can get permission using authorizationStatus() function of the CNContactStore class, In our application, we create one compilation handler method which will return the status of the authorization. It will look like: –

func requestedForAccess(complitionHandler:@escaping ( _ accessGranted:Bool)->Void)
{
    let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts)
    switch authorizationStatus 
    {
        case .authorized:
            complitionHandler(true)
        case .notDetermined,.denied:
            self.contactStore.requestAccess(for: CNEntityType.contacts  ) { (access, accessError) in
                if access{
                    complitionHandler(access)
                }else{
                    if authorizationStatus == .denied{
                        let message="Allow access to contacts to this app using settings app"
                        DispatchQueue.main.async{
                            print(message)
                        }
                    }
                }
        }
        default:
        complitionHandler(false)
    }
}

Step 2

Now we create another method to fetch contact list, it returns the array of CNContact.

CNContact: CNContact represents the value of contact properties such as familyName, givenName, phoneNumbers of contact.

func getContact() -> [CNContact] 
{
    var results:[CNContact] = []
    let keyToContactFetch = [CNContactGivenNameKey as CNKeyDescriptor, CNContactFamilyNameKey as CNKeyDescriptor, CNContactMiddleNameKey as CNKeyDescriptor, CNContactEmailAddressesKey as CNKeyDescriptor,CNContactPhoneNumbersKey as CNKeyDescriptor]
    let fetchRequest = CNContactFetchRequest(keysToFetch: keyToContactFetch)
    fetchRequest.sortOrder = CNContactSortOrder.userDefault
    do
    {
        try self.contactStore.enumerateContacts(with: fetchRequest, usingBlock: { (contact, stop) in
            print(contact.phoneNumbers.first?.value ?? "no")
            results.append(contact)
        })
    }
    catch let error as NSError 
    {
        print(error.localizedDescription)
    }
    return results
}

Step 3

Create a Bean Class named ContactBean.swift It will look like below: –

import Foundation
import UIKit

public class ContactBean: NSObject 
{
    var firstName:String?
    var lastName: String?
    var mobileNumber: String?
    var email: String?
    
    init(firstName:String?, lastName:String?,mobileNumber:String?,email:String?) 
    {
        self.firstName = firstName
        self.lastName = lastName
        self.mobileNumber = mobileNumber
        self.email = email
    }
}

Step 4

Now create a separate object for each phone number and store it as ContactBean object in the arrContactsDicts array.

For example, one contact object has two numbers then both numbers will be stored in an array as a single contact.

Our create Contact array method looks like below: –

func createDictOfContactList(compilationClosure: @escaping (_ arrContectDict:NSMutableArray)->())
{
    self.arrContactsDicts.removeAllObjects()
    DispatchQueue.global(qos: .background).async
    {
        self.requestedForAccess
        { (accessGranted) in
            if(accessGranted)
            {
                for contact in self.getContact()
                {
                    for tempContact:CNLabeledValue in contact.phoneNumbers
                    {
                        if contact.givenName.lowercased() == "spam" || contact.givenName.lowercased() == "identified as spam"
                        {
                            continue
                        }
                        var emailAddress = ""
                        if (contact.emailAddresses as NSArray).count != 0
                        {
                            emailAddress = (contact.emailAddresses.first!).value as String
                        }
                        self.getFinalNumber(tempContact, compilationClosure: { (finalNumber) in
                            
                            if (finalNumber != "")
                            {
                                let dict = ContactBean.init(firstName: contact.givenName, lastName: contact.familyName, mobileNumber: finalNumber, email: emailAddress)
                                self.arrContactsDicts.add(dict)
                            }
                        })
                    }
                }
                compilationClosure(self.arrContactsDicts)
            }
            else
            {
                compilationClosure([])
            }
        }
    }
}

Note: Must create operation in background queue so it does not freeze application UI

Step 5

After creating the array we are going to compare with the stored contact list.

I) Fetch contact list: We use getObjectForEntity method to fetch contact list stored in the contact entity of Core Data.

let arrContactList =  CoreDataManager.sharedInstance.getObjectsforEntity(strEntity: "Contact", ShortBy: "", isAscending: false, predicate: nil, groupBy: "", taskContext: CoreDataManager.sharedInstance.bGManagedObjectContext) as! NSArray

II) Check arrContactList is empty?

After fetching contacts in CoreData, we will check arrContactList is empty or not. If yes, then we will store all the contacts in contact entity of CoreData.

We create saveNewContactInDb method to save a new contact, thus it will be:

func saveNewContactInDb(dict:NSMutableArray, completionHandler:@escaping (_ success:Bool) -> ()) -> Void 
{
    DispatchQueue.global(qos: .background).async 
    {
        for newContact in dict
        {
            let tempContact = newContact as! ContactBean
            let objContact = (CoreDataManager.sharedInstance.createObjectForEntity(entityName: "Contact", taskContext: sharedCoreDataManager.bGManagedObjectContext) ) as! Contact
            objContact.firstName = tempContact.firstName!
            objContact.lastName = tempContact.lastName!
            objContact.email = tempContact.email!
            objContact.mobileNumber = tempContact.mobileNumber!
            sharedCoreDataManager.saveContextInBG()
        }
        completionHandler(true)
    }
}

III) If arrContactlist > 0 then check for update or need to delete.

> Copy the newly fetch contact list in temp array which is tempArrContactDict.

> Filter contact of arrContactList from arrContactDict. If it is found in arrContactDicts then check for update and update it and remove from tempArrContactDict.

> If contact does not found in arrContactDict, then we can consider it as deleted from the contact directory of the device. So we need to delete from our local storage.

> At the end of for loop, we can check if any contact is still available in tempArrContactDict. Which means it was newly inserted in the contact directory and need to add in our CoreData entity.

> So finally our syncPhoneBookContactsWithLocalDB looks like below:

func syncPhoneBookContactsWithLocalDB(completionHandler:@escaping (_ success:Bool) -> ())
{
    if(appDelegate.isContactSyncInProgress == false)
    {
        appDelegate.isContactSyncInProgress = true
        self.createDictOfContactList
        { (arrDictContactList) in
            
            DispatchQueue.global(qos: .background).async 
            {
                let arrContactList =  CoreDataManager.sharedInstance.getObjectsforEntity(strEntity: "Contact", ShortBy: "", isAscending: false, predicate: nil, groupBy: "", taskContext: CoreDataManager.sharedInstance.bGManagedObjectContext) as! NSArray
                if arrContactList.count > 0
                {
                    //Check for require update contact in db
                    let tempArrContactDict = self.arrContactsDicts
                    for contact in arrContactList
                    {
                        let tempContactDb = contact as! Contact
                        let mobileNumber = tempContactDb.mobileNumber
                        if(mobileNumber != "")
                        {
                            let arrFilterContact = self.arrContactsDicts.filter({($0 as! ContactBean).mobileNumber! == mobileNumber!})
                            if arrFilterContact.count > 0
                            {
                                let filterdContact = arrFilterContact.first as! ContactBean
                                if(filterdContact.firstName != tempContactDb.firstName || filterdContact.lastName != tempContactDb.lastName || filterdContact.email != tempContactDb.email)
                                {
                                    tempContactDb.firstName = filterdContact.firstName
                                    tempContactDb.lastName = filterdContact.lastName
                                    tempContactDb.email = filterdContact.email
                                    sharedCoreDataManager.saveContextInBG()
                                    //Update contact if it was updated in contact
                                }
                                tempArrContactDict.remove(arrFilterContact.first as Any)
                            }
                            else
                            {
                                //This contact is not available in newContact dict so it means this contact is deleted in contact directory so we need to delete from out data base.
                                print(“This Contact is deleted from db --> \(String(describing: tempContactDb.mobileNumber))")
                                sharedCoreDataManager.deleteObject(object: tempContactDb, taskContext: CoreDataManager.sharedInstance.bGManagedObjectContext)
                            }
                        }
                    }
                    if(tempArrContactDict.count > 0)
                    {
                        //After sync new contact list with local db there is still contact is avalilabe in contact dict so it means there is newly insertad contact so insert new contact in the local data base
                        print(“These is new contact added")
                        self.saveNewContactInDb(dict:tempArrContactDict,completionHandler: { (success) in
                            completionHandler(true)
                        })
                    }
                    else
                    {
                        print("There is no any new contact added")
                        completionHandler(true)
                    }
                }
                else
                {
                    //Save all new contact in db
                    print("Fresh contact list add")
                    self.saveNewContactInDb(dict: self.arrContactsDicts, completionHandler: { (success) in
                        completionHandler(true)
                    })
                }
            }
        }
    }
}

*In the above code, we check for contact sync is in progress using an appDelegate global variable named isContactSyncInProgress (Boolean variable).

Note: We pass taskContext as bgManageObjectContext and save using saveContextBG() method because it is possible that we may have a large contact list and it can freeze the application. So we use bGManageObjectContext as a separate context so our application will work smooth and fetch contacts and store in the background.

Now our Contact utility class is ready to use.

Sync contacts from AppDelegate

We will sync contact when the application starts and it enters in the foreground.
Here is the AppDelegate to sync contact: –

class AppDelegate: UIResponder, UIApplicationDelegate 
{
    var window: UIWindow?
    var isContactSyncInProgress = false
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    {       
        ContactUtility.sharedInstance.syncPhoneBookContactsWithLocalDB { (success) in
            self.isContactSyncInProgress = false
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ContactSync"), object: nil, userInfo: nil)
            print("Contact sync successfully")
        }
        return true
    }
	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.
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) 
        {
            ContactUtility.sharedInstance.syncPhoneBookContactsWithLocalDB 
            { (success) in

                self.isContactSyncInProgress = false
                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ContactSync"), object: nil, userInfo: nil)
                print("Contact sync successfully")
            }
        }        
    }
}

Furthermore, we have complete demo code which you can find here on GitHub. If you find any issues or have any questions, feel free to comment below or contact us.

Don’t miss the next post!

Loading

Related Posts