[iOS, iPadOS]How to use Core Data with CloudKit

Core Data and CloudKit can now be linked automatically from iOS 13 and iPadOS 13, and I tried out immediately.

 

Environment: swift 5, Xcode 11.0

 

Preparation

1.Check both [Use Core Data] and [Use CloudKit] when creating a project

 

2.After creating the project, select [TARGETS]-[Signing & Capabilities] and add [iCloud]. After that, check [CloudKit] and enter the name of [Containers].

Let's look AppDelegate

Once you have done this, take a look at the code that is automatically generated in AppDelegate.swift.

import UIKit

import CoreData

 

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

 

    // MARK: - Core Data stack

 

    lazy var persistentContainer: NSPersistentCloudKitContainer = {

        /*

         The persistent container for the application. This implementation

         creates and returns a container, having loaded the store for the

         application to it. This property is optional since there are legitimate

         error conditions that could cause the creation of the store to fail.

        */

        let container = NSPersistentCloudKitContainer(name: "CoreDataWithCloudKitSample")

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

            if let error = error as NSError? {

                // 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.

                 

                /*

                 Typical reasons for an error here include:

                 * The parent directory does not exist, cannot be created, or disallows writing.

                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.

                 * The device is out of space.

                 * The store could not be migrated to the current model version.

                 Check the error message to determine what the actual problem was.

                 */

                fatalError("Unresolved error \(error), \(error.userInfo)")

            }

        })

        return container

    }()

 

    // MARK: - Core Data Saving support

 

    func saveContext () {

        let context = 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)")

            }

        }

    }

}

Until now, the class that accesses Core Data Stack was NSPersistentContainer, but when [use CloudKit] is checked, it changes to NSPersistentCloudKitContainer.

By using NSPersistentCloudKitContainer, linkage with CloudKit is automatically performed.

 

Create Entity of Core Data

Select the .xcdatamodeId file from the project and press [Add Entity] to create the Entity.

The Binary Data type is provided for saving UIImage data.

 

Next, execute [Editor]-[Create NSManagedObject Subclass ...] of Xcode to generate NSManagedObject class of "GoodsMaster".

 

The following two files are generated.

 

[GoodsMaster+CoreDataClass.swift]

import Foundation

import CoreData

 

@objc(GoodsMaster)

public class GoodsMaster: NSManagedObject {

 

}

 

[GoodsMaster+CoreDataProperties.swift]

import Foundation

import CoreData

 

 

extension GoodsMaster {

 

    @nonobjc public class func fetchRequest() -> NSFetchRequest<GoodsMaster> {

        return NSFetchRequest<GoodsMaster>(entityName: "GoodsMaster")

    }

 

    @NSManaged public var code: String?

    @NSManaged public var costRate: Double

    @NSManaged public var image: Data?

    @NSManaged public var imageOrientation: Int16

    @NSManaged public var name: String?

    @NSManaged public var salesPrice: Int64

 

}

 

Looking at Configurations in the .xcdatamodeId file, [Used with CloudKit] is checked.

Manipulating CoreData data

The data operation is exactly the same as the previous Core Data.

 

NsManagedContext

Core Data operations are performed with NSManagedContext.

Get NSManagedContext through AppDelegate.

let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext

 

INSERT

import UIKit

import CoreData

 

  private func insertData(code:String, name:String, costRate:Double, salesPrice:Int, image:UIImage!){

        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext

        

        let objectEntity:NSManagedObject = NSEntityDescription.insertNewObject(forEntityName: "GoodsMaster", into: managedContext) as NSManagedObject

        objectEntity.setValue(code, forKey: "code")

        objectEntity.setValue(name, forKey: "name")

        objectEntity.setValue(costRate, forKey: "costRate")

        objectEntity.setValue(salesPrice, forKey: "salesPrice")

        

        if (image != nil){

           //UIImage converted to NSData.

           let imageData = image.jpegData(compressionQuality: 1.0)

        

            //Get orientation of UIImage.

            var imageOrientation:Int = 0

            if (image.imageOrientation == UIImage.Orientation.down){

                imageOrientation = 2

            }else{

                imageOrientation = 1

            }

            

            objectEntity.setValue(imageData, forKey: "image")

            objectEntity.setValue(imageOrientation, forKey: "imageOrientation")

        }

        

        do {

            try managedContext.save()

        }catch let error {

            NSLog("\(error)")

        }

 

    }

 

Set the data to be inserted into NSManagedObject.

If you execute NSManagerdContext's save method after setting the data, the set data will be saved.

 

SELECT

import UIKit

import CoreData

 

  private struct GoodsMaster {

        var code:String

        var name:String

        var costRate:Double

        var salesPrice:Int

        var image:UIImage!

 

    }

 

   private func searchData(minSalesPrice:Int, maxSalesPrice:Int){

        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext

        

        //Search criteria

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "GoodsMaster")

        fetchRequest.returnsObjectsAsFaults = false

        let predicate = NSPredicate(format: "salesPrice >= %d and salesPrice <= %d", minSalesPrice, maxSalesPrice)

        fetchRequest.predicate = predicate

        

        //Sort conditions

        let sortDescriptor1 = NSSortDescriptor(key: "salesPrice", ascending: false)

        let sortDescriptor2 = NSSortDescriptor(key: "costRate", ascending: true)

        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]

        

        do{

            let fetchResults: Array = try managedContext.fetch(fetchRequest)

            

            self.goodsMasters.removeAll()

            for fetchResult in fetchResults {

                let code = (fetchResult as AnyObject).value(forKey: "code") as! String

                let name = (fetchResult as AnyObject).value(forKey: "name") as! String

                let costRate = (fetchResult as AnyObject).value(forKey: "costRate") as! Double

                let salesPrice = (fetchResult as AnyObject).value(forKey: "salesPrice") as! Int

                

                let imageData = (fetchResult as AnyObject).value(forKey: "image") as! NSData?

                let imageOrientation = (fetchResult as AnyObject).value(forKey: "imageOrientation") as! Int?

                

                var image:UIImage! = nil

                if (imageData != nil){

                    image = UIImage(data: imageData! as Data)

                    if (imageOrientation == 2) {

                        image = UIImage(cgImage: image!.cgImage!, scale: image!.scale, orientation: UIImage.Orientation.down)

                    }

                }

                let goodsMaster = GoodsMaster(code: code, name: name, costRate: costRate, salesPrice: salesPrice, image: image)

                self.goodsMasters.append(goodsMaster)

            }

        }catch let error {

            NSLog("\(error)")

        }

    }

 

Information required for search is set in the NSFetchRequest.

Set the search condition to NSPredicate and the sort condition to NSSortDescriptor. And pass NSPredicate and NSSortDescriptor to NSFetchRequest.

Search is executed by executing NSManagedContext fetch method with NSFetchRequest as an argument.

 

UPDATE

import UIKit

import CoreData

 

  private func updateData(whereCode:String, updateName:String, updateCostRate:Double, updateSalesPrice:Int, updateImage:UIImage!){

        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext

        

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "GoodsMaster")

        fetchRequest.returnsObjectsAsFaults = false

        let predicate = NSPredicate(format: "code == %@", whereCode)

        fetchRequest.predicate = predicate

        

        do {

            let fetchResults: Array = try managedContext.fetch(fetchRequest)

            

            for fetchResult in fetchResults {

                let managedObject = fetchResult as! NSManagedObject

                managedObject.setValue(updateName, forKey: "name")

                managedObject.setValue(updateCostRate, forKey: "costRate")

                managedObject.setValue(updateSalesPrice, forKey: "salesPrice")

                if (updateImage != nil){

                    let imageData = updateImage.jpegData(compressionQuality: 1.0)

                    

                    var imageOrientation:Int = 0

                    if (updateImage.imageOrientation == UIImage.Orientation.down){

                        imageOrientation = 2

                    }else{

                        imageOrientation = 1

                    }

                    

                    managedObject.setValue(imageData, forKey: "image")

                    managedObject.setValue(imageOrientation, forKey: "imageOrientation")

                }

                try managedContext.save()

            }

        }catch let error {

            NSLog("\(error)")

        }

    }

 

UPDATE is performed by first searching the data, changing the searched value, and saving.

The code looks like a combination of INSERT and SELECT.

 

DELETE

import UIKit

import CoreData

 

  private func deleteData(whereCode:String){

        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

        let managedContext: NSManagedObjectContext = appDelegate.persistentContainer.viewContext

        

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "GoodsMaster")

        fetchRequest.returnsObjectsAsFaults = false

        let predicate = NSPredicate(format: "code == %@", whereCode)

        fetchRequest.predicate = predicate

        

        do {

            let fetchResults: Array = try managedContext.fetch(fetchRequest)

            

            for fetchResult in fetchResults {

                let managedObject = fetchResult as! NSManagedObject

                

                managedContext.delete(managedObject)

            }

            try managedContext.save()

        }catch let error {

            NSLog("\(error)")

        }

    }

 

DELETE is performed by first searching for data and deleting that data.

 

Demo

Let's try Core Data data linkage between devices using CloudKit.

Prepare an iPhone and iPad with iCloud settings in advance.

 

1.INSERT data on iPhone.

 

2.Select [TARGETS]-[Signing & Capabilities]-[iCloud] in Xcode and click [CloudKit Dashboard] to display the CloudKit Dashboard.

 

3.If you select [Schema] in CloudKit Dashboard, you can see that the Entity created in Core Data is automatically created on CloudKit.

   Amazing!!!

 

4.If you select [Data] on the CloudKit Dashboard, you can see that the data registered on the iPhone exists on CloudKit.

 

5.When the program is started on the iPad and SELECT is executed, the data inserted by the iPhone is displayed.

Sample Program

The source code of this sample program is saved on GitHub.

https://github.com/naosekig/CoreDataWithCloudKitSample