1. Code
  2. Mobile Development
  3. iOS Development

Building a Shopping List Application With CloudKit: Adding Records

Scroll to top
This post is part of a series called Building a Shopping List Application With CloudKit.
Building a Shopping List Application With CloudKit: Introduction
Building a Shopping List Application With CloudKit: Adding Relationships

In the first tutorial of this series, we explored the CloudKit framework and infrastructure. We also laid the foundation for the sample application that we're going to build, a shopping list application. In this tutorial, we are focusing on adding, editing, and removing shopping lists.

Prerequisites

As I mentioned in the previous tutorial, I will be using Xcode 9 and Swift 4. If you are using an older version of Xcode, then keep in mind that you might be using a different version of the Swift programming language.

In this tutorial, we will continue working with the project we created in the first tutorial. You can download it from GitHub (tag adding_records).

1. Setting Up CocoaPods

The shopping list application will make use of the SVProgressHUD library, a popular library created by Sam Vermette that makes it easy to display a progress indicator. You can add the library manually to your project, but I strongly recommend using CocoaPods for managing dependencies. Are you new to CocoaPods? Read this introductory tutorial to CocoaPods to get up to speed.

Step 1: Creating a Podfile

Open Finder and navigate to the root of your Xcode project. Create a new file, name it Podfile, and add the following lines of Ruby to it.

1
# Uncomment the next line to define a global platform for your project

2
# platform :ios, '9.0'

3
4
target 'Lists' do
5
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks

6
  use_frameworks!
7
8
  pod 'SVProgressHUD', '~> 1.1'
9
10
end

The first line specifies the platform, iOS, and the project's deployment target, iOS 9.0. The second line is important if you're using Swift. Swift does not support static libraries, but CocoaPods does provide the option since version 0.36 to use frameworks. We then specify the dependencies for the Lists target of the project. Replace Lists with your target's name if your target is named differently.

Step 2: Installing Dependencies

Open Terminal, navigate to the root of your Xcode project, and run pod install. This will do a number of things for you, such as installing the dependencies specified in Podfile and creating an Xcode workspace.

After completing the CocoaPods setup, close the project and open the workspace CocoaPods created for you. The latter is very important. Open the workspace, not the project. The workspace includes two projects, the Lists project and a project named Pods.

2. Listing Shopping Lists

Step 1: Housekeeping

We're ready to refocus on the CloudKit framework. First, however, we need to do some housekeeping by renaming the ViewController class to the ListsViewController class.

Start by renaming ViewController.swift to ListsViewController.swift. Open ListsViewController.swift and change the name of the ViewController class to the ListsViewController class.

Next, open Main.storyboard, expand View Controller Scene in the Document Outline on the left, and select View Controller. Open the Identity Inspector on the right and change Class to ListsViewController.

Open the Identity Inspector on the right and change Class to ListsViewControllerOpen the Identity Inspector on the right and change Class to ListsViewControllerOpen the Identity Inspector on the right and change Class to ListsViewController

Step 2: Adding a Table View

When the user opens the application, they're presented with their shopping lists. We'll display the shopping lists in a table view. Let's start by setting up the user interface. Select the Lists View Controller in the Lists View Controller Scene and choose Embed In > Navigation Controller from Xcode's Editor menu.

Add a table view to the view controller's view and create the necessary layout constraints for it. With the table view selected, open the Attributes Inspector and set Prototype Cells to 1. Select the prototype cell and set Style to Basic and Identifier to ListCell.

Select the prototype cell and set Style to Basic and Identifier to ListCellSelect the prototype cell and set Style to Basic and Identifier to ListCellSelect the prototype cell and set Style to Basic and Identifier to ListCell

With the table view selected, open the Connections Inspector. Connect the table view's dataSource and delegate outlets to the Lists View Controller.

Step 3: Empty State

Even though we're only creating a sample application to illustrate how CloudKit works, I'd like to display a message if something goes wrong or if no shopping lists were found on iCloud. Add a label to the view controller, make it as large as the view controller's view, create the necessary layout constraints for it, and center the label's text.

Since we're dealing with network requests, I also want to display an activity indicator view as long as the application is waiting for a response from iCloud. Add an activity indicator view to the view controller's view and center it in its parent view. In the Attributes Inspector, tick the checkbox labeled Hides When Stopped.

Lists View ControllerLists View ControllerLists View Controller

Step 4: Connecting Outlets

Open ListsViewController.swift and declare an outlet for the label, the table view, and the activity indicator view. This is also a good time to make the ListsViewController class conform to the UITableViewDataSource and UITableViewDelegate protocols.

Note that I've also added an import statement for the SVProgressHUD framework and that I've declared a static constant for the reuse identifier of the prototype cell we created in the storyboard.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
6
    static let ListCell = "ListCell"
7
    @IBOutlet weak var tableView: UITableView!
8
    @IBOutlet weak var messageLabel: UILabel!
9
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
10
    ...
11
    
12
}

Head back to the storyboard and connect the outlets with the corresponding views in the Lists View Controller Scene.

Step 5: Preparing the Table View

Before we fetch data from iCloud, we need to make sure the table view is ready to display the data. We first need to create a property, lists, to hold the records we're about to fetch. Remember that records are instances of the CKRecord class. This means the property that will hold the data from iCloud is of type [CKRecord], an array of CKRecord instances.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
6
    static let ListCell = "ListCell"
7
    @IBOutlet weak var tableView: UITableView!
8
    @IBOutlet weak var messageLabel: UILabel!
9
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
10
    
11
    var lists = [CKRecord]()
12
    
13
    ...
14
    
15
}

To get started, we need to implement three methods of the UITableViewDataSource protocol:

  • numberOfSectionsInTableView(_:)
  • numberOfRowsInSection(_:)
  • cellForRowAtIndexPath(_:)

If you have any experience working with table views, then the implementation of each of these methods is straightforward. However, cellForRowAtIndexPath(_:) may require some explanation. Remember that a CKRecord instance is a supercharged dictionary of key-value pairs. To access the value of a particular key, you invoke objectForKey(_:) on the CKRecord object. That's what we do in cellForRowAtIndexPath(_:). We fetch the record that corresponds to the table view row and ask it for the value for key "name". If the key-value pair doesn't exist, we display a dash to indicate the list doesn't have a name yet.

1
// MARK: -

2
// MARK: UITableView Delegate Methods

3
extension ListsViewController{
4
    
5
    func numberOfSections(in tableView: UITableView) -> Int {
6
        return 1
7
    }
8
    
9
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
10
        return lists.count
11
    }
12
    
13
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
14
        // Dequeue Reusable Cell

15
        let cell = tableView.dequeueReusableCell(withIdentifier: ListsViewController.ListCell, for: indexPath)
16
        
17
        // Configure Cell

18
        cell.accessoryType = .detailDisclosureButton
19
        
20
        // Fetch Record

21
        let list = lists[indexPath.row]
22
        
23
        if let listName = list.object(forKey: "name") as? String {
24
            // Configure Cell

25
            cell.textLabel?.text = listName
26
            
27
        } else {
28
            cell.textLabel?.text = "-"
29
        }
30
        
31
        return cell
32
    }
33
34
}

Step 6: Preparing the User Interface

There's one more step for us to take: preparing the user interface. In the view controller's viewDidLoad method, remove the fetchUserRecordID method call and invoke setupView, a helper method.

1
override func viewDidLoad() {
2
    super.viewDidLoad()
3
    
4
    setupView()
5
}

The setupView method prepares the user interface for fetching the list of records. We hide the label and the table view, and tell the activity indicator view to start animating.

1
// MARK: -

2
// MARK: View Methods

3
private func setupView() {
4
    tableView.hidden = true
5
    messageLabel.hidden = true
6
    activityIndicatorView.startAnimating()
7
}

Build and run the application on a device or in the iOS Simulator. If you've followed the above steps, you should see an empty view with a spinning activity indicator view in the center.

Busy Pretending to Be Fetching DataBusy Pretending to Be Fetching DataBusy Pretending to Be Fetching Data

Step 7: Creating a Record Type

Before we fetch any records, we need to create a record type for a shopping list in the CloudKit dashboard. The CloudKit dashboard is a web application that lets developers manage the data stored on Apple's iCloud servers.

Select the project in the Project Navigator and choose the Lists target from the list of targets. Open the Capabilities tab at the top and expand the iCloud section. Below the list of iCloud containers, click the button labeled CloudKit Dashboard.

Open CloudKit DashboardOpen CloudKit DashboardOpen CloudKit Dashboard

Sign in with your developer account and make sure the Lists application is selected in the top left. On the left, select Record Types from the Schema section. Every application has by default a Users record type. To create a new record type, click the plus button at the top of the third column. We will follow Apple's naming convention and name the record type Lists, not List.

Adding a New Record TypeAdding a New Record TypeAdding a New Record Type

Note that the first field is automatically created for you. Create a field name and set Field Type to String. Don't forget to click the Save button at the bottom to create the Lists record type. We'll revisit the CloudKit Dashboard later in this series.

Next, enable indexing for your document property by going to the Indexes tab and adding a new SORTABLE and another QUERYABLE index type for name, and click Save.

Adding SORTABLE and QUERYABLE indexingAdding SORTABLE and QUERYABLE indexingAdding SORTABLE and QUERYABLE indexing

Finally, go to the SECURITY ROLES tab and, for the purposes of this development exercise, check all the checkboxes to ensure your user has access to the table. 

Step 8: Performing a Query

With the Lists record type created, it's finally time to fetch some records from iCloud. The CloudKit framework provides two APIs to interact with iCloud: a convenience API and an API based on the NSOperation class. We will use both APIs in this series, but we're going to keep it simple for now and use the convenience API.

In Xcode, open ListsViewController.swift and invoke the fetchLists method in viewDidLoad. The fetchLists method is another helper method. Let's take a look at the method's implementation.

1
override func viewDidLoad() {
2
    super.viewDidLoad()
3
    
4
    setupView()
5
    fetchLists()
6
}

Because a shopping list record is stored in the user's private database, we first get a reference to the default container's private database. To fetch the user's shopping lists, we need to perform a query on the private database, using the CKQuery class.

1
private func fetchLists() {
2
        // Fetch Private Database

3
        let privateDatabase = CKContainer.default().privateCloudDatabase
4
        
5
        // Initialize Query

6
        let query = CKQuery(recordType: "Lists", predicate: NSPredicate(value: true))
7
        
8
        // Configure Query

9
        query.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
10
        
11
        // Perform Query

12
        privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
13
            records?.forEach({ (record) in
14
                
15
                guard error == nil else{
16
                    print(error?.localizedDescription as Any)
17
                    return
18
                }
19
                
20
                print(record.value(forKey: "name") ?? "")
21
                self.lists.append(record)
22
                DispatchQueue.main.sync {
23
                    self.tableView.reloadData()
24
                    self.messageLabel.text = ""
25
                    updateView()
26
                }
27
            })
28
    
29
        }
30
    }

We initialize a CKQuery instance by invoking the init(recordType:predicate:) designated initializer, passing in the record type and an NSPredicate object. 

Before we execute the query, we set the query's sortDescriptors property. We create an array containing an NSSortDescriptor object with key "name" and ascending set to true.

Executing the query is as simple as calling performQuery(_:inZoneWithID:completionHandler:) on privateDatabase, passing in query as the first argument. The second parameter specifies the identifier of the record zone on which the query will be performed. By passing in nil, the query is performed on the default zone of the database, and we get an instance of each record returned from the query. 

At the end of the method, we invoke updateView. In this helper method, we update the user interface based on the contents of the lists property.

1
    private func updateView(){
2
        let hasRecords = self.lists.count > 0
3
        
4
        self.tableView.isHidden = !hasRecords
5
        messageLabel.isHidden = hasRecords
6
        activityIndicatorView.stopAnimating()
7
    }

Build and run the application to test what we've got so far. We currently don't have any records, but we'll fix that in the next section of this tutorial.

No Records FoundNo Records FoundNo Records Found

3. Adding a Shopping List

Step 1: Creating the AddListViewController Class

Because adding and editing a shopping list are very similar, we are going to implement both at the same time. Create a new file and name it AddListViewController.swift. Open the newly created file and create a UIViewController subclass named AddListViewController. At the top, add import statements for the UIKitCloudKit, and SVProgressHUD frameworks. Declare two outlets, one of type UITextField! and one of type UIBarButtonItem!. Last but not least, create two actions, cancel(_:) and save(_:).

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
class AddListViewController: UIViewController {
6
    
7
    @IBOutlet weak var nameTextField: UITextField!
8
    @IBOutlet weak var saveButton: UIBarButtonItem!
9
    
10
    @IBAction func cancel(sender: AnyObject) {
11
        
12
    }
13
    
14
    @IBAction func save(sender: AnyObject) {
15
        
16
    }
17
    
18
}

Step 2: Creating the User Interface

Open Main.storyboard and add a view controller to the storyboard. With the view controller selected, open the Identity Inspector on the right and set Class to AddListViewController.

Adding a View Controller and setting it to AddListViewcontrollerAdding a View Controller and setting it to AddListViewcontrollerAdding a View Controller and setting it to AddListViewcontroller

The user will be able to navigate to the add list view controller by tapping a button in the lists view controller. 

Drag a bar button item from the Object Library to the navigation bar of the lists view controller. With the bar button item selected, open the Attributes Inspector and set System Item to Add. Press Control and drag from the bar button item to the add the list view controller and select Show Detail from the menu that appears.

Select the segue you just created and set Identifier to ListDetail in the Attributes Inspector on the right.

Adding ListDetail to inspector of segueAdding ListDetail to inspector of segueAdding ListDetail to inspector of segue

Add two bar button items to the navigation bar of the add list view controller, one on the left and one on the right. Set the System Item of the left bar button item to Cancel and that of the right bar button item to Save. Finally, add a text field to the add list view controller. Center the text field and set its Alignment to center in the Attributes Inspector.

Add List View ControllerAdd List View ControllerAdd List View Controller

Finally, connect the outlets and actions you created in AddListViewController.swift to the corresponding user interface elements in the scene.

Step 3: AddListViewControllerDelegate Protocol

Before we implement the AddListViewController class, we need to declare a protocol that we'll use to communicate from the add list view controller to the lists view controller. The protocol defines two methods, one for adding and one for updating a shopping list. This is what the protocol looks like.

1
protocol AddListViewControllerDelegate {
2
    func controller(controller: AddListViewController, didAddList list: CKRecord)
3
    func controller(controller: AddListViewController, didUpdateList list: CKRecord)
4
}

We also need to declare three properties: one for the delegate, one for the shopping list that is created or updated, and a helper variable that indicates whether we're creating a new shopping list or editing an existing record.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
protocol AddListViewControllerDelegate {
6
    func controller(controller: AddListViewController, didAddList list: CKRecord)
7
    func controller(controller: AddListViewController, didUpdateList list: CKRecord)
8
}
9
10
class AddListViewController: UIViewController {
11
    
12
    @IBOutlet weak var nameTextField: UITextField!
13
    @IBOutlet weak var saveButton: UIBarButtonItem!
14
    
15
    var delegate: AddListViewControllerDelegate?
16
    var newList: Bool = true
17
    
18
    var list: CKRecord?
19
    
20
    @IBAction func cancel(sender: AnyObject) {
21
        
22
    }
23
    
24
    @IBAction func save(sender: AnyObject) {
25
        
26
    }
27
    
28
}

The implementation of the AddListViewController class is straightforward. The methods related to the view lifecycle are short and easy to understand. In viewDidLoad, we first invoke the setupView helper method. We'll implement this method in a moment. We then update the value of the newList helper variable based on the value of the list property. If list is equal to nil, then we know that we're creating a new record. In viewDidLoad, we also add the view controller as an observer for UITextFieldTextDidChangeNotification notifications.

1
override func viewDidLoad() {
2
        super.viewDidLoad()
3
        
4
        self.setupView()
5
        
6
        // Update Helper

7
        self.newList = self.list == nil
8
        
9
        // Add Observer

10
        let notificationCenter = NotificationCenter.default
11
        notificationCenter.addObserver(self, selector: #selector(AddListViewController.textFieldTextDidChange(notification:)), name: NSNotification.Name.UITextFieldTextDidChange, object: nameTextField)
12
    }
13
    
14
    override func viewDidAppear(_ animated: Bool) {
15
        super.viewDidAppear(animated)
16
        nameTextField.becomeFirstResponder()
17
    }

In viewDidAppear(_:), we call becomeFirstResponder on the text field to present the keyboard to the user.

In setupView, we invoke two helper methods, updateNameTextField and updateSaveButton. In updateNameTextField, we populate the text field if list is not nil. In other words, if we're editing an existing record, then we populate the text field with the name of that record.

The updateSaveButton method is in charge of enabling and disabling the bar button item in the top right. We only enable the save button if the name of the shopping list is not an empty string.

1
private func setupView() {
2
        updateNameTextField()
3
        updateSaveButton()
4
    }
5
    
6
    // MARK: -

7
    private func updateNameTextField() {
8
        if let name = list?.object(forKey: "name") as? String {
9
            nameTextField.text = name
10
        }
11
    }
12
    
13
    // MARK: -

14
    private func updateSaveButton() {
15
        let text = nameTextField.text
16
        
17
        if let name = text {
18
            saveButton.isEnabled = !name.isEmpty
19
        } else {
20
            saveButton.isEnabled = false
21
        }
22
    }
23
    

Step 4: Implementing Actions

The cancel(_:) action is as simple as it gets. We pop the top view controller from the navigation stack. The save(_:) action is more interesting. In this method, we extract the user's input from the text field and get a reference to the default container's private database.

If we're adding a new shopping list, then we create a new CKRecord instance by invoking init(recordType:), passing in RecordTypeLists as the record type. We then update the name of the shopping list by setting the value of the record for the key "name".

Because saving a record involves a network request and can take a non-trivial amount of time, we show a progress indicator. To save a new record or any changes to an existing record, we call saveRecord(_:completionHandler:) on privateDatabase, passing in the record as the first argument. The second argument is another completion handler that is invoked when saving the record completes, successfully or unsuccessfully.

The completion handler accepts two arguments, an optional CKRecord and an optional NSError. As I mentioned before, the completion handler can be invoked on any thread, which means that we need to code against that. We do this by explicitly invoking the processResponse(_:error:) method on the main thread.

1
@IBAction func cancel(sender: AnyObject) {
2
        self.dismiss(animated: true, completion: nil) 
3
    }
4
    
5
    @IBAction func save(sender: AnyObject) {
6
        
7
        // Helpers

8
        let name = self.nameTextField.text! as NSString
9
        
10
        // Fetch Private Database

11
        let privateDatabase = CKContainer.default().privateCloudDatabase
12
        
13
        if list == nil {
14
            list = CKRecord(recordType: "Lists")
15
        }
16
        
17
        // Configure Record

18
        list?.setObject(name, forKey: "name")
19
        
20
        // Show Progress HUD

21
        SVProgressHUD.show()
22
        
23
        // Save Record

24
        privateDatabase.save(list!) { (record, error) -> Void in
25
            DispatchQueue.main.sync {
26
                // Dismiss Progress HUD

27
                SVProgressHUD.dismiss()
28
                
29
                // Process Response

30
                self.processResponse(record: record, error: error)
31
            }
32
33
        }
34
    }

In processResponse(_:error:), we verify if an error was thrown. If we did run into problems, we display an alert to the user. If everything went smoothly, we notify the delegate and pop the view controller from the navigation stack.

1
// MARK: -

2
    // MARK: Helper Methods

3
    private func processResponse(record: CKRecord?, error: Error?) {
4
        var message = ""
5
        
6
        if let error = error {
7
            print(error)
8
            message = "We were not able to save your list."
9
            
10
        } else if record == nil {
11
            message = "We were not able to save your list."
12
        }
13
        
14
        if !message.isEmpty {
15
            // Initialize Alert Controller

16
            let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
17
            
18
            // Present Alert Controller

19
            present(alertController, animated: true, completion: nil)
20
            
21
        } else {
22
            // Notify Delegate

23
            if newList {
24
                delegate?.controller(controller: self, didAddList: list!)
25
            } else {
26
                delegate?.controller(controller: self, didUpdateList: list!)
27
            }
28
            
29
            // Pop View Controller

30
            self.dismiss(animated: true, completion: nil)
31
32
        }
33
    }

Last but not least, when the view controller receives a UITextFieldTextDidChangeNotification notification, it invokes updateSaveButton to update the save button.

1
// MARK: -

2
// MARK: Notification Handling

3
func textFieldTextDidChange(notification: NSNotification) {
4
    updateSaveButton()
5
}

Step 5: Tying Everything Together

In the ListsViewController class, we still need to take care of a few things. Let's start by conforming the class to the AddListViewControllerDelegate protocol.

1
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {
2
...

This also means that we need to implement the methods of the AddListViewControllerDelegate protocol. In the controller(_:didAddList:) method, we add the new record to the array of CKRecord objects. We then sort the array of records, reload the table view, and invoke updateView on the view controller.

1
// MARK: -

2
// MARK: Add List View Controller Delegate Methods

3
func controller(controller: AddListViewController, didAddList list: CKRecord) {
4
    // Add List to Lists

5
    lists.append(list)
6
    
7
    // Sort Lists

8
    sortLists()
9
    
10
    // Update Table View

11
    tableView.reloadData()
12
13
    // Update View

14
    updateView()
15
}

The sortLists method is pretty basic. We call sortInPlace on the array of records, sorting the array based on the record's name.

1
private func sortLists() {
2
        self.lists.sort {
3
            var result = false
4
            let name0 = $0.object(forKey: "name") as? String
5
            let name1 = $1.object(forKey: "name") as? String
6
            
7
            if let listName0 = name0, let listName1 = name1 {
8
                result = listName0.localizedCaseInsensitiveCompare(listName1) == .orderedAscending
9
            }
10
            
11
            return result
12
        }
13
    }

The implementation of the second method of the AddListViewControllerDelegate protocol, controller(_:didUpdateList:), looks almost identical. Because we're not adding a record, we only need to sort the array of records and reload the table view. There's no need to call updateView on the view controller since the array of records is, by definition, not empty.

1
func controller(controller: AddListViewController, didUpdateList list: CKRecord) {
2
    // Sort Lists

3
    sortLists()
4
    
5
    // Update Table View

6
    tableView.reloadData()
7
}

To edit a record, the user needs to tap the accessory button of a table view row. This means that we need to implement the tableView(_:accessoryButtonTappedForRowWithIndexPath:) method of the UITableViewDelegate protocol. Before we implement this method, declare a helper property, selection, to store the user's selection.

1
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
2
    static let ListCell = "ListCell"
3
    
4
    @IBOutlet weak var messageLabel: UILabel!
5
    @IBOutlet weak var tableView: UITableView!
6
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
7
    
8
    var lists = [CKRecord]()
9
    
10
    var selection: Int?
11
    
12
    ...
13
    
14
}

In tableView(_:accessoryButtonTappedForRowWithIndexPath:), we store the user's selection in selection and tell the view controller to perform the segue that leads to the add list view controller.

1
// MARK: -

2
    // MARK: Segue Life Cycle

3
    func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
4
       
5
        tableView.deselectRow(at: indexPath as IndexPath, animated: true)
6
        
7
        // Save Selection

8
        selection = indexPath.row
9
        
10
        // Perform Segue

11
        performSegue(withIdentifier: "ListDetail", sender: self)
12
    }
13
    
14
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
15
        
16
        // Fetch Destination View Controller

17
        let addListViewController = segue.destination as! AddListViewController
18
        
19
        // Configure View Controller

20
        addListViewController.delegate = self
21
        
22
        if let selection = selection {
23
            // Fetch List

24
            let list = lists[selection]
25
            
26
            // Configure View Controller

27
            addListViewController.list = list
28
        }
29
    }

We're almost there. When the segue with identifier ListDetail is performed, we need to configure the AddListViewController instance that is pushed onto the navigation stack. We do this in prepareForSegue(_:sender:).

The segue hands us a reference to the destination view controller, the AddListViewController instance. We set the delegate property, and, if a shopping list is updated, we set the view controller's list property to the selected record.

1
// MARK: -

2
// MARK: Segue Life Cycle

3
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
4
    guard let identifier = segue.identifier else { return }
5
    
6
    switch identifier {
7
    case SegueListDetail:
8
        // Fetch Destination View Controller

9
        let addListViewController = segue.destinationViewController as! AddListViewController
10
        
11
        // Configure View Controller

12
        addListViewController.delegate = self
13
        
14
        if let selection = selection {
15
            // Fetch List

16
            let list = lists[selection]
17
            
18
            // Configure View Controller

19
            addListViewController.list = list
20
        }
21
    default:
22
        break
23
    }
24
}

Build and run the application to see the result. You should now be able to add a new shopping list and edit the name of existing shopping lists.

4. Deleting Shopping Lists

Adding the ability to delete shopping lists isn't much extra work. The user should be able to delete a shopping list by swiping a table view row from right to left and tapping the delete button that is revealed. To make this possible, we need to implement two more methods of the UITableViewDataSource protocol:

  • tableView(_:canEditRowAtIndexPath:)
  • tableView(_:commitEditingStyle:forRowAtIndexPath:)

The implementation of tableView(_:canEditRowAtIndexPath:) is trivial, as you can see below.

1
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
2
    return true
3
}

In tableView(_:commitEditingStyle:forRowAtIndexPath:), we fetch the correct record from the array of records and invoke deleteRecord(_:) on the view controller, passing in the record that needs to be deleted.

1
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {    guard editingStyle == .delete else { return }
2
    
3
    // Fetch Record

4
    let list = lists[indexPath.row]
5
    
6
    // Delete Record

7
    deleteRecord(list)
8
}

The deleteRecord(_:) method should look familiar by now. We show a progress indicator and call deleteRecordWithID(_:completionHandler:) on the default container's private database. Note that we're passing in the record identifier, not the record itself. The completion handler accepts two arguments, an optional CKRecordID and an optional NSError.

1
private func deleteRecord(_ list: CKRecord) {
2
        // Fetch Private Database

3
        let privateDatabase = CKContainer.default().privateCloudDatabase
4
        
5
        // Show Progress HUD

6
        SVProgressHUD.show()
7
        
8
        // Delete List

9
        privateDatabase.delete(withRecordID: list.recordID) { (recordID, error) -> Void in
10
            DispatchQueue.main.sync {
11
                SVProgressHUD.dismiss()
12
                
13
                // Process Response

14
                self.processResponseForDeleteRequest(list, recordID: recordID, error: error)
15
            }
16
        }
17
    }

In the completion handler, we dismiss the progress indicator and invoke processResponseForDeleteRequest(_:recordID:error:) on the main thread. In this method, we inspect the values of recordID and error that the CloudKit API has given us, and we update message accordingly. If the delete request was successful, then we update the user interface and the array of records.

1
private func processResponseForDeleteRequest(_ record: CKRecord, recordID: CKRecordID?, error: Error?) {
2
        var message = ""
3
        
4
        if let error = error {
5
            print(error)
6
            message = "We are unable to delete the list."
7
            
8
        } else if recordID == nil {
9
            message = "We are unable to delete the list."
10
        }
11
        
12
        if message.isEmpty {
13
            // Calculate Row Index

14
            let index = self.lists.index(of: record)
15
            
16
            if let index = index {
17
                // Update Data Source

18
                self.lists.remove(at: index)
19
                
20
                if lists.count > 0 {
21
                    // Update Table View

22
                    self.tableView.deleteRows(at: [NSIndexPath(row: index, section: 0) as IndexPath], with: .right)
23
                    
24
                } else {
25
                    // Update Message Label

26
                    messageLabel.text = "No Records Found"
27
                    
28
                    // Update View

29
                    updateView()
30
                }
31
            }
32
            
33
        } else {
34
            // Initialize Alert Controller

35
            let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
36
            
37
            // Present Alert Controller

38
            present(alertController, animated: true, completion: nil)
39
        }
40
    }
41
    

That's it. It's time to properly test the application with some data. Run the application on a device or in the iOS Simulator and add a few shopping lists. You should be able to add, edit, and delete shopping lists.

Conclusion

Even though this article is fairly long, it's good to remember that we only briefly interacted with the CloudKit API. The convenience API of the CloudKit framework is lightweight and easy to use.

This tutorial, however, has also illustrated that your job as a developer isn't limited to interacting with the CloudKit API. It's important to handle errors, show the user when a request is in progress, update the user interface, and tell the user what is going on.

In the next article of this series, we take a closer look at relationships by adding the ability to fill a shopping list with items. An empty shopping list isn't of much use, and it certainly isn't fun. Leave any questions you have in the comments below or reach out to me on Twitter.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.