An iOS 10 CloudKit Example

PreviousTable of ContentsNext
An Introduction to CloudKit SharingAn iOS 10 CloudKit Subscription Example


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


With the basics of the CloudKit Framework covered in the previous chapters, many of the concepts covered in those chapters will now be explored in greater detail through the implementation of an example iOS project. The app created in this chapter will demonstrate the use of the CloudKit Framework to create, update, query and delete records in a private CloudKit database. In the next chapter, the project will be extended further to demonstrate the use of CloudKit subscriptions to notify users when new records are added to the application’s database. Later, the project will be further modified to implement CloudKit Sharing.

About the Example CloudKit Project

The steps outlined in this chapter are intended to make use of a number of key features of the CloudKit framework. The example will take the form of the prototype for an application designed to appeal to users while looking for a new home. The app allows the users to store the addresses, take photos and enter notes about properties visited. This information will be stored on iCloud using the CloudKit Framework and will include the ability to save, delete, update and search for records.

Before starting on this project it is important to note that membership to the Apple Developer Program will be required as outlined in Joining the Apple Developer Program.

Creating the CloudKit Example Project

Launch Xcode and create a new Single View Application project named CloudKitDemo, with the programming language set to Swift and the Devices menu set to Universal.

Once the project has been created, the first step is to enable CloudKit entitlements for the application. Select the CloudKitDemo entry listed at the top of the project navigator panel and, in the main panel, click on the Capabilities tab. Within the resulting list of entitlements, make sure that the iCloud option is switched to the On position, that the CloudKit services option is enabled and the Containers section is configured to use the default container:


Enabling CloudKit support in an Xcode project

Figure 49-1


Note that if you are using the pre-created version of the project from the book sample download, it will be necessary to assign your own Apple Developer ID to the project. To achieve this, switch to the General view and use the Team menu in the Identity section to select or add your Apple Developer ID to the project.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Designing the User Interface

The user interface layout for this project will consist of an Image View, Text Field, Text View and a Toolbar containing five Bar Button Items. Begin the design process by selecting the Main.storyboard file in the project navigator panel so that the storyboard loads into the Interface Builder environment.

Drag and drop views from the Object Library panel onto the storyboard canvas and resize, position and configure the views so that the layout resembles that outlined in Figure 49 2, making sure to stretch the views horizontally so that they align with the margins of the containing view (represented by the dotted blue lines that appear when resizing a view). After adding the Text View and before adding the Image View, select the Text View object, display the Auto Layout Add New Constraints menu and add a Height constraint.


The user interface layout for the example iOS CloudKit app

Figure 49-2


Select the Text Field view, display the Attributes Inspector and enter Address into the Placeholder attribute field. Select the Image View and change the Content Mode attribute to Aspect Fit.

Click on the white background of the view controller layout so that none of the views in the layout are currently selected. Using the Resolve Auto Layout Issues menu located in the lower right-hand corner of the Interface Builder panel, select the Add Missing Constraints menu option.

Compile and run the application on physical devices and simulators to confirm that the layout responds correctly to different screen sizes.

With the Text View selected in the storyboard, display the Attributes Inspector in the Utilities panel and delete the example Latin text.

Establishing Outlets and Actions

Display the Assistant Editor, select the Text Field view in the storyboard and Ctrl-click and drag from the view to a position beneath the “class ViewController” declaration and release the line. In the resulting connection panel, establish an outlet connection named addressField. Repeat these steps for the Text View and Image View, establishing Outlets named commentsField and imageView respectively.

Using the same technique establish Action connections from the five Bar Button Items to action methods named saveRecord, queryRecord, selectPhoto, updateRecord and deleteRecord respectively. Once the connections have been established, the ViewController.swift file should read as follows:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var addressField: UITextField!
    @IBOutlet weak var commentsField: UITextView!
    @IBOutlet weak var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func saveRecord(_ sender: AnyObject) {
    }

    @IBAction func queryRecord(_ sender: AnyObject) {
    }

    @IBAction func selectPhoto(_ sender: AnyObject) {
    }

    @IBAction func updateRecord(_ sender: AnyObject) {
    }

    @IBAction func deleteRecord(_ sender: AnyObject) {
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Accessing the Private Database

Since the information entered into the app is only relevant to the current user, the data entered into the application will be stored on iCloud using the application’s private cloud database. Obtaining a reference to the private cloud database first involves gaining a reference to the application’s container. Within the ViewController.swift file make the following additions, noting the inclusion of import statements for frameworks that will be needed later in the tutorial, together with variables to store the current database record, record zone and photo image URL:

import UIKit
import CloudKit
import MobileCoreServices

class ViewController: UIViewController {

    @IBOutlet weak var addressField: UITextField!
    @IBOutlet weak var commentsField: UITextView!
    @IBOutlet weak var imageView: UIImageView!

    let container = CKContainer.default
    var privateDatabase: CKDatabase?
    var currentRecord: CKRecord?
    var photoURL: URL?
    var recordZone: CKRecordZone?

    override func viewDidLoad() {
        super.viewDidLoad()
        privateDatabase = container().privateCloudDatabase
        recordZone = CKRecordZone(zoneName: "HouseZone")

         privateDatabase?.save(recordZone!, 
		completionHandler: {(recordzone, error) in
            if (error != nil) {
                self.notifyUser("Record Zone Error", 
			message: "Failed to create custom record zone.")
            } else {
                print("Saved record zone")
            }
         }) 
    }
.
.
}

Note that the code added to the viewDidLoad method also initializes the recordZone variable with a CKRecordZone object configured with the name “HouseZone” and saves it to the private database.

Hiding the Keyboard

It is important to include code to ensure that the user has a way to hide the keyboard after entering text into the two text areas in the user interface. Within the ViewController.swift file, override the touchesBegan method to hide the keyboard when the user taps the background view of the user interface:

override func touchesBegan(_ touches: Set<UITouch>,
                   with event: UIEvent?) {
    addressField.endEditing(true)
    commentsField.endEditing(true)
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Implementing the selectPhoto method

The purpose of the Photo button in the toolbar is to allow the user to include a photo in the database record. Begin by declaring that the View Controller class implements the UIImagePickerControllerDelegate and UINavigationControllerDelegate protocols, then locate the template selectPhoto action method within the ViewController.swift file and implement the code as follows:

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
.
.
@IBAction func selectPhoto(_ sender: AnyObject) {

    let imagePicker = UIImagePickerController()

    imagePicker.delegate = self
    imagePicker.sourceType =
           UIImagePickerControllerSourceType.photoLibrary
    imagePicker.mediaTypes = [kUTTypeImage as String]

    self.present(imagePicker, animated: true,
                     completion:nil)
}
.
.
}

When executed, the code will cause the image picker view controller to be displayed from which the user can select a photo from the photo library on the device to be stored along with the current record in the application’s private cloud database. The final step in the photo selection process is to implement the delegate methods which will be called when the user has either made a photo selection or cancels the selection process:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    self.dismiss(animated: true, completion: nil)
    let image =
        info[UIImagePickerControllerOriginalImage] as! UIImage
    imageView.image = image
    photoURL = saveImageToFile(image)
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    self.dismiss(animated: true, completion: nil)
}

In both cases the image picker view controller is dismissed from view. In the case of a photo being selected, the image is displayed within the application via the Image View instance before being written to the file system of the device and the corresponding URL stored in the photoURL variable for later reference. Clearly the code expects the image file writing to be performed by a method named saveImageToFile which must now be implemented:

func saveImageToFile(_ image: UIImage) -> URL
{
    let filemgr = FileManager.default

    let dirPaths = filemgr.urls(for: .documentDirectory,
                                in: .userDomainMask)

    let fileURL = dirPaths[0].appendingPathComponent("currentImage.jpg")

    if let renderedJPEGData =
                UIImageJPEGRepresentation(image, 0.5) {
        try! renderedJPEGData.write(to: fileURL)
    }

    return fileURL
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

The method obtains a reference to the application’s Documents directory and constructs a path to an image file named currentImage.jpg. The image is then written to the file in JPEG format using a compression rate of 0.5 in order to reduce the amount of storage used from the application’s iCloud allowance. The resulting file URL is then returned to the calling method.

Saving a Record to the Cloud Database

When the user has entered an address and comments and selected a photo, the data is ready to be saved to the cloud database. This will require that code be added to the saveRecord method as follows:

@IBAction func saveRecord(_ sender: AnyObject) {

    if (photoURL == nil) {
        notifyUser("No Photo", 
	message: "Use the Photo option to choose a photo for the record")
        return
    }

    let asset = CKAsset(fileURL: photoURL!)
    let myRecord = CKRecord(recordType: "Houses", 
					zoneID: (recordZone?.zoneID)!)

    myRecord.setObject(addressField.text as CKRecordValue?, 
					forKey: "address")

    myRecord.setObject(commentsField.text as CKRecordValue?, 
					forKey: "comment")

    myRecord.setObject(asset, forKey: "photo")


    let modifyRecordsOperation = CKModifyRecordsOperation(
		recordsToSave: [myRecord], 
		recordIDsToDelete: nil)

    modifyRecordsOperation.timeoutIntervalForRequest = 10
    modifyRecordsOperation.timeoutIntervalForResource = 10

    modifyRecordsOperation.modifyRecordsCompletionBlock = 
		{ records, recordIDs, error in
        if let err = error {
            self.notifyUser("Save Error", message:
                err.localizedDescription)
        } else {
            DispatchQueue.main.async {
                self.notifyUser("Success",
                                message: "Record saved successfully")
            }
            self.currentRecord = myRecord
        }
    }
    privateDatabase?.add(modifyRecordsOperation)
} 

The method begins by verifying that the user has selected a photo to include in the database record. If one has not yet been selected the user is notified that a photo is required (the notifyUser method will be implemented in the next section).

Next a new CKAsset object is created and initialized with the URL to the photo image previously selected by the user. A new CKRecord instance is then created and assigned a record type of “Houses”. The record is then initialized with the content of the Text Field and Text View, the CKAsset object containing the photo image and ID of the zone with which the record is to be associated.

Once the record is ready to be saved, a CKModifyRecordsOperation object is initialized with the record object and then executed against the user’s private database. A completion handler is used to report the success or otherwise of the save operation. Since this involves working with the user interface, it is dispatched to the main thread to prevent the application from locking up. The new record is then saved to the currentRecord variable where it can be referenced should the user subsequently decide to update or delete the record.

Implementing the notifyUser Method

The notifyUser method takes as parameters two string values representing a title and message to be displayed to the user. These strings are then used in the construction of an Alert View. Implement this method as follows:

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

func notifyUser(_ title: String, message: String) -> Void
{
    let alert = UIAlertController(title: title,
            			message: message,
        			preferredStyle: .alert)

    let cancelAction = UIAlertAction(title: "OK",
            style: .cancel, handler: nil)

    alert.addAction(cancelAction)
    self.present(alert, animated: true,
                completion: nil)
}

Testing the Record Saving Method

Compile and run the application on a device or simulator on which an iCloud account has been configured. Enter text into the address and comments fields and select an image from the photo library (images can be added to the photo library of a simulator instance by dragging images from a Finder window and dropping them onto the simulator window).

Tap the Save button to save the record and wait for the alert to appear indicating that the record has been saved successfully. Note that CloudKit operations are performed asynchronously so the amount of time for a successful save to be reported will vary depending on the size of the record being saved, network speed and responsiveness of the iCloud servers.

Once the record has been saved, return to the Capabilities screen for the application (Figure 49 1) and click on the CloudKit Dashboard button. This will launch the default web browser on the system and load the CloudKit Dashboard portal. Enter your Apple developer login and password and, once the dashboard has loaded, select the CloudKitDemo application from the drop down menu (marked as A in Figure 49-3). From the navigation panel, select HouseZone (B) located under PRIVATE DATA and make sure that the Houses record type is selected in the record listing panel (C). The saved record should be listed and the address, comments and photo asset shown in the main panel (D):


The CloudKit Dashboard

Figure 49-3


If HouseZone is not listed in the private data section of the navigation panel, make sure that the device or simulator on which the app is running is signed in using the same Apple ID as that used to log into the CloudKit Dashboard.

Having verified that the record was saved, the next task is to implement the remaining action methods.

Searching for Cloud Database Records

The Query feature of the application allows a record matching a specified address to be retrieved and displayed to the user. Locate the template queryRecord method in the ViewController.swift file and implement the body of the method to perform the query operation:

@IBAction func queryRecord(_ sender: AnyObject) {

    let predicate = NSPredicate(format: "address = %@", 
				addressField.text!)

    let query = CKQuery(recordType: "Houses", predicate: predicate)

    privateDatabase?.perform(query, inZoneWith: recordZone?.zoneID,
                  completionHandler: ({results, error in

        if (error != nil) {
            DispatchQueue.main.async() {
                self.notifyUser("Cloud Access Error",
                    message: error!.localizedDescription)
            }
        } else {
            if results!.count > 0 {

                let record = results![0]
                self.currentRecord = record

                DispatchQueue.main.async() {

                    self.commentsField.text =
                       record.object(forKey: "comment") as! String

                    let photo =
                       record.object(forKey: "photo") as! CKAsset

                    let image = UIImage(contentsOfFile:
                        photo.fileURL.path)

                    self.imageView.image = image
                    self.photoURL = self.saveImageToFile(image!)
                }
            } else {
                DispatchQueue.main.async() {
                    self.notifyUser("No Match Found",
                     message: "No record matching the address was found")
                }
            }
        }
    }))
} 

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Despite the seeming complexity of this method it actually performs some very simple tasks. First a new predicate instance is configured to indicate that the query is to search for records where the address field matches the content of the address entered by the user into the application user interface. This predicate is then used to create a new CKQuery object together with the record type for which the search is to be performed.

The perform(query:) method of the private cloud database object is then called, passing through the query object together with the record zone ID and a completion handler to be executed when the query returns (as with all CloudKit operations, queries are performed asynchronously).

In the event of an error or a failure to find a match, the user is notified via an alert. In the event that one or more matches are found, the first matching record is extracted from the results array and the data and image contained therein displayed to the user via the user interface outlet connections.

Compile and run the application, enter the address used when the record was saved in the previous section and tap the Query button. After a short delay, the display should update with the content of the record.

Updating Cloud Database Records

When updating existing records in a cloud database, the ID of the record to be updated must be referenced since this is the only way to uniquely identify one record from another. In the case of the CloudKitDemo application, this means that the record must already have been loaded into the application and assigned to the currentRecord variable, thereby allowing the recordID to be obtained and used for the update operation. With these requirements in mind, implement the code in the stub updateRecord method as follows:

@IBAction func updateRecord(_ sender: AnyObject) {

   if let record = currentRecord {

        let asset = CKAsset(fileURL: photoURL!)

        record.setObject(addressField.text as CKRecordValue?, 
				forKey: "address")
        record.setObject(commentsField.text as CKRecordValue?, 
				forKey: "comment")
        record.setObject(asset, forKey: "photo")

        privateDatabase?.save(record, completionHandler:
          ({returnRecord, error in
            if let err = error {
                DispatchQueue.main.async() {
                    self.notifyUser("Update Error",
                          message: err.localizedDescription)
                }
            } else {
                DispatchQueue.main.async() {
                    self.notifyUser("Success", message:
                            "Record updated successfully")
                }
            }
        }))
    } else {
        notifyUser("No Record Selected", message:
                "Use Query to select a record to update")
    }
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

This method performs similar tasks to the saveRecord method. This time, however, instead of creating a new CKRecord instance, the existing record assigned to the currentRecord variable is updated with the latest text and photo content entered by the user. When the save operation is performed, CloudKit will identify that the record has an ID that matches an existing record in the database and update that matching record with the latest data provided by the user.

Deleting a Cloud Record

CloudKit record deletions can be achieved by calling the deleteRecordWithID method of the CKDatabase instance, passing through as arguments the ID of the record to be deleted and a completion handler to be executed when the deletion operation returns. As with the updateRecord method, a deletion can only be performed when the record to be deleted has already been selected within the application and assigned to the currentRecord variable:

@IBAction func deleteRecord(_ sender: AnyObject) {
    if let record = currentRecord {

        privateDatabase?.delete(withRecordID: record.recordID,
                completionHandler: ({returnRecord, error in
            if let err = error {
                DispatchQueue.main.async() {
                    self.notifyUser("Delete Error", message:
                                     err.localizedDescription)
                }
            } else {
                DispatchQueue.main.async() {
                    self.notifyUser("Success", message:
                           "Record deleted successfully")
                }
            }
        }))
    } else {
        notifyUser("No Record Selected", message:
                        "Use Query to select a record to delete")
    }
}

Testing the Application

With the basic functionality of the application implemented, compile and run it on a device or simulator instance and add, query, update and delete records to verify that the application functions as intended.

Summary

CloudKit provides an easy way to implement the storage and retrieval of iCloud based database records from within iOS applications. The objective of this chapter has been to demonstrate in practical terms the techniques available to save, search, update and delete database records stored in an iCloud database using the CloudKit convenience API.

One area of CloudKit that was not addressed in this chapter involves the use of CloudKit subscriptions to notify the user when changes have been made to a cloud database. In the next chapter, entitled An iOS 10 CloudKit Subscription Example the CloudKitDemo application will be extended to add this functionality.


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book



PreviousTable of ContentsNext
An Introduction to CloudKit SharingAn iOS 10 CloudKit Subscription Example