1. Code
  2. Mobile Development
  3. iOS Development

Building a Shopping List Application With CloudKit: Adding Relationships

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: Adding Records
Building a Shopping List Application With CloudKit: Sharing Shopping Items

In the previous post of this series, we added the ability to add, update, and remove shopping lists. A shopping list without any items in it isn't very useful, though. In this tutorial, we'll add the ability to add, update, and remove items from a shopping list. This means that we'll be working with references and the CKReference class.

We'll also take a closer look at the data model of the shopping list application. How easy is it to make changes to the data model, and how does the application respond to changes we make in the CloudKit Dashboard?

Prerequisites

Remember that I will be using Xcode 9 and Swift 3. If you are using an older version of Xcode, then keep in mind that you are using a different version of the Swift programming language.

In this tutorial, we will continue where we left off in the previous post of this series. You can download or clone the project from GitHub.

1. Shopping List Details

Currently, the user can modify the name of a shopping list by tapping the detail disclosure indicator, but the user should also be able to see the contents of a shopping list by tapping one in the lists view controller. To make this work, we first need a new UIViewController subclass.

Step 1: Creating ListViewController

The ListViewController class will display the contents of a shopping list in a table view. The interface of the ListViewController class looks similar to that of the ListsViewController class. We import the CloudKit and SVProgressHUD frameworks and conform the class to the UITableViewDataSource and UITableViewDelegate protocols. Because we'll be using a table view, we declare a constant, ItemCell, that will serve as a cell reuse identifier.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
6
    
7
    static let ItemCell = "ItemCell"
8
    
9
    @IBOutlet weak var messageLabel: UILabel!
10
    @IBOutlet weak var tableView: UITableView!
11
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
12
    
13
    var list: CKRecord!
14
    var items = [CKRecord]()
15
    
16
    var selection: Int?
17
18
    ...
19
20
}

We declare three outlets: messageLabel of type UILabel!tableView of type UITableView!, and activityIndicatorView of type UIActivityIndicatorView!. The list view controller keeps a reference to the shopping list it is displaying in the list property, which is of type CKRecord!. The items in the shopping list are stored in the items property, which is of type [CKRecord]. Finally, we use a helper variable, selection, to keep track of which item in the shopping list the user has selected. This will become clear later in this tutorial.

Step 2: Creating the User Interface

Open Main.storyboard, add a view controller, and set its class to ListViewController in the Identity Inspector. Select the prototype cell of the lists view controller, press the Control key, and drag from the prototype cell to the list view controller. Choose Show from the menu that pops up, and set the identifier to List in the Attributes Inspector.

Add a table view, a label, and an activity indicator view to the view controller's view. Don't forget to wire up the outlets of the view controller as well as those of the table view.

Select the table view and set Prototype Cells to 1 in the Attributes Inspector. Select the prototype cell, and set Style to Right Detail, Identifier to ItemCell, and Accessory to Disclosure Indicator. This is what the view controller should look like when you're finished.

List View ControllerList View ControllerList View Controller

Step 3: Configuring the View Controller

Before we revisit the CloudKit framework, we need to prepare the view controller for the data it's going to receive. Start by updating the implementation of viewDidLoad. We set the view controller's title to the name of the shopping list and invoke two helper methods, setupView and fetchItems.

1
// MARK: -

2
// MARK: View Life Cycle

3
override func viewDidLoad() {
4
    super.viewDidLoad()
5
    
6
    // Set Title

7
    title = list.objectForKey("name") as? String
8
    
9
    setupView()
10
    fetchItems()
11
}

The setupView method is identical to the one we implemented in the ListsViewController class.

1
// MARK: -

2
// MARK: View Methods

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

While we're at it, let's also implement another familiar helper method, updateView. In updateView, we update the user interface of the view controller based on the items stored in the items property.

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

I'm going to leave fetchItems empty for now. We'll revisit this method once we're finished setting up the list view controller.

1
// MARK: -

2
// MARK: Helper Methods

3
private func fetchItems() {
4
    
5
}

Step 4: Table View Data Source Methods

We're almost ready to take the application for another test run. Before we do, we need to implement the UITableViewDataSource protocol. If you've read the previous installments of this series, then the implementation will look familiar.

1
// MARK: -

2
// MARK: Table View Data Source Methods

3
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
4
    return 1;
5
}
6
7
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
8
    return items.count
9
}
10
11
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
12
    // Dequeue Reusable Cell

13
    let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath)
14
    
15
    // Configure Cell

16
    cell.accessoryType = .DetailDisclosureButton
17
    
18
    // Fetch Record

19
    let item = items[indexPath.row]
20
    
21
    if let itemName = item.objectForKey("name") as? String {
22
        // Configure Cell

23
        cell.textLabel?.text = itemName
24
        
25
    } else {
26
        cell.textLabel?.text = "-"
27
    }
28
    
29
    return cell
30
}
31
32
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
33
    return true
34
}

Step 5: Handling Selection

To tie everything together, we need to revisit the ListsViewController class. Start by implementing the tableView(_:didSelectRowAtIndexPath:) method of the UITableViewDelegate protocol.

1
// MARK: -

2
// MARK: Table View Delegate Methods

3
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
4
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
5
}

We also need to update prepareForSegue(segue:sender:) to handle the segue we created a few moments ago. This means that we need to add a new case to the switch statement.

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 SegueList:
8
        // Fetch Destination View Controller

9
        let listViewController = segue.destinationViewController as! ListViewController
10
        
11
        // Fetch Selection

12
        let list = lists[tableView.indexPathForSelectedRow!.row]
13
        
14
        // Configure View Controller

15
        listViewController.list = list
16
    case SegueListDetail:
17
        ...
18
    default:
19
        break
20
    }
21
}

To satisfy the compiler, we also need to declare the SegueList constant at the top of ListsViewController.swift.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
let RecordTypeLists = "Lists"
6
7
let SegueList = "List"
8
let SegueListDetail = "ListDetail"
9
10
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {
11
    ...
12
}

Build and run the application to see if everything is wired up correctly. Because we haven't implemented the fetchItems method yet, no items will be displayed. That's something we need to fix.

2. Fetching Items

Step 1: Create a Record Type

Before we can fetch items from the CloudKit backend, we need to create a new record type in the CloudKit Dashboard. Navigate to the CloudKit Dashboard, create a new record type, and name it Items. Each item should have a name, so create a new field, set the field name to name, and set the field type to String.

Each item should also know to which shopping list it belongs. That means each item needs a reference to its shopping list. Create a new field, set the field name to list, and set the field type to Reference. The Reference field type was designed for this exact purpose, managing relationships.

Item Record TypeItem Record TypeItem Record Type

Head back to Xcode, open ListsViewController.swift, and declare a new constant at the top for the Items record type.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
let RecordTypeLists = "Lists"
6
let RecordTypeItems = "Items"
7
8
let SegueList = "List"
9
let SegueListDetail = "ListDetail"
10
11
class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {
12
    ...
13
}

Step 2: Fetching Items

Open ListViewController.swift and navigate to the fetchItems method. The implementation is similar to the fetchLists method of the ListsViewController class. There is an important difference, though.

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

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

6
    let reference = CKReference(recordID: list.recordID, action: .DeleteSelf)
7
    let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %@", reference))
8
    
9
    // Configure Query

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

13
    privateDatabase.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
14
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
15
            // Process Response on Main Thread

16
            self.processResponseForQuery(records, error: error)
17
        })
18
    }
19
}

The difference between fetchItems and fetchLists is the predicate we pass to the CKQuery initializer. We're not interested in every item in the user's private database. We're only interested in the items that are associated with a particular shopping list. This is reflected in the predicate of the CKQuery instance.

We create the predicate by passing in a CKReference instance, which we create by invoking init(recordID:action:). This method accepts two arguments: a CKRecordID instance that references the shopping list record and a CKReferenceAction instance that determines what happens when the shopping list is deleted.

Reference actions are very similar to delete rules in Core Data. If the referenced object (the shopping list in this example) is deleted, then the CloudKit framework inspects the reference action to determine what should happen to the records that hold a reference to the deleted record. The CKReferenceAction enum has two member values:

  • None: If the reference is deleted, nothing happens to the records referencing the deleted record.
  • DeleteSelf: If the reference is deleted, every record referencing the deleted record is also deleted.

Because no item should exist without a shopping list, we set the reference action to DeleteSelf.

The processResponseForQuery(records:error:) method contains nothing new. We process the response of the query and update the user interface accordingly.

1
private func processResponseForQuery(records: [CKRecord]?, error: NSError?) {
2
    var message = ""
3
    
4
    if let error = error {
5
        print(error)
6
        message = "Error Fetching Items for List"
7
        
8
    } else if let records = records {
9
        items = records
10
        
11
        if items.count == 0 {
12
            message = "No Items Found"
13
        }
14
        
15
    } else {
16
        message = "No Items Found"
17
    }
18
    
19
    if message.isEmpty {
20
        tableView.reloadData()
21
    } else {
22
        messageLabel.text = message
23
    }
24
    
25
    updateView()
26
}

Build and run the application. You won't see any items yet, but the user interface should update to reflect that the shopping list is empty.

3. Adding Items

Step 1: Creating AddItemViewController

It's time to implement the ability to add items to a shopping list. Start by creating a new UIViewController subclass, AddItemViewController. The interface of the view controller is similar to that of the AddListViewController class.

At the top, we import the CloudKit and SVProgressHUD frameworks. We declare the AddItemViewControllerDelegate protocol, which will serve the same purpose as the AddListViewControllerDelegate protocol. The protocol defines two methods, one for adding items and one for updating items.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
protocol AddItemViewControllerDelegate {
6
    func controller(controller: AddItemViewController, didAddItem item: CKRecord)
7
    func controller(controller: AddItemViewController, didUpdateItem item: CKRecord)
8
}
9
10
class AddItemViewController: UIViewController {
11
    
12
    @IBOutlet weak var nameTextField: UITextField!
13
    @IBOutlet weak var saveButton: UIBarButtonItem!
14
    
15
    var delegate: AddItemViewControllerDelegate?
16
    var newItem: Bool = true
17
    
18
    var list: CKRecord!
19
    var item: CKRecord?
20
    
21
    ...
22
    
23
}

We declare two outlets, a text field and a bar button item. We also declare a property for the delegate and a helper variable, newItem, that helps us determine whether we're creating a new item or updating an existing item. Finally, we declare a property list for referencing the shopping list to which the item will be added and a property item for the item we're creating or updating.

Before we create the user interface, let's implement two actions that we'll need in the storyboard, cancel(_:) and save(_:). We'll update the implementation of the save(_:) action later in this tutorial.

1
// MARK: -

2
// MARK: Actions

3
@IBAction func cancel(sender: AnyObject) {
4
    navigationController?.popViewControllerAnimated(true)
5
}
6
7
@IBAction func save(sender: AnyObject) {
8
    navigationController?.popViewControllerAnimated(true)
9
}

Step 2: Creating the User Interface

Open Main.storyboard, add a bar button item to the navigation bar of the list view controller, and set System Item to Add in the Attributes Inspector. Drag a view controller from the Object Library and set its class to AddItemViewController. Create a segue from the bar button item we just created to the add item view controller. Choose Show from the menu that pops up, and set the segue's identifier to ItemDetail.

Add two bar button items to the navigation bar of the add item view controller, a cancel button on the left and a save button on the right. Connect each bar button item to its corresponding action. Add a text field to the view controller's view and don't forget to connect the outlets of the view controller. This is what the add item view controller should look like when you're finished.

Add Item View ControllerAdd Item View ControllerAdd Item View Controller

Step 3: Configuring the View Controller

The implementation of the add item view controller contains nothing that we haven't covered yet. There's one exception, though, which we'll discuss in a moment. Let's start by configuring the view controller in viewDidLoad.

We invoke setupView, a helper method, and update the value of newItem. If the item property is equal to nilnewItem is equal to true. This helps us determine whether we're creating or updating a shopping list item.

We also add the view controller as an observer for notifications of type UITextFieldTextDidChangeNotification. This means that the view controller is notified when the contents of the nameTextField change.

1
// MARK: -

2
// MARK: View Life Cycle

3
override func viewDidLoad() {
4
    super.viewDidLoad()
5
    
6
    setupView()
7
    
8
    // Update Helper

9
    newItem = item == nil
10
    
11
    // Add Observer

12
    let notificationCenter = NSNotificationCenter.defaultCenter()
13
    notificationCenter.addObserver(self, selector: "textFieldTextDidChange:", name: UITextFieldTextDidChangeNotification, object: nameTextField)
14
}

In viewDidAppear(animated:), we show the keyboard by calling becomeFirstResponder on nameTextField.

1
override func viewDidAppear(animated: Bool) {
2
    nameTextField.becomeFirstResponder()
3
}

The setupView method invokes two helper methods, updateNameTextField and updateSaveButton. The implementation of these helper methods is straightforward. In updateNameTextField, we populate the text field. In updateSaveButton, we enable or disable the save button based on the contents of the text field.

1
// MARK: -

2
// MARK: View Methods

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

9
private func updateNameTextField() {
10
    if let name = item?.objectForKey("name") as? String {
11
        nameTextField.text = name
12
    }
13
}
14
15
// MARK: -

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

Before we take a look at the updated implementation of the save(_:) method, we need to implement the textFieldDidChange(_:) method. All we do is invoke updateSaveButton to enable or disable the save button.

1
// MARK: -

2
// MARK: Notification Handling

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

Step 4: Saving Items

The save(_:) method is the most interesting method of the AddItemViewController class, because it shows us how to work with CloudKit references. Take a look at the implementation of the save(_:) method below.

Most of its implementation should look familiar since we covered saving records in the AddListViewController class. What interests us most is how the item keeps a reference to its shopping list. We first create a CKReference instance by invoking the designated initializer, init(recordID:action:). We covered the details of creating a CKReference instance a few minutes ago when we created the query for fetching shopping list items.

Telling the item about this reference is easy. We invoke setObjec(_:forKey:) on the item property, passing in the CKReference instance as the value and "list" as the key. The key corresponds to the field name we assigned in the CloudKit Dashboard. Saving the item to iCloud is identical to what we've covered before. That's how easy it is to work with CloudKit references.

1
@IBAction func save(sender: AnyObject) {
2
    // Helpers

3
    let name = nameTextField.text
4
    
5
    // Fetch Private Database

6
    let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase
7
    
8
    if item == nil {
9
        // Create Record

10
        item = CKRecord(recordType: RecordTypeItems)
11
        
12
        // Initialize Reference

13
        let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf)
14
        
15
        // Configure Record

16
        item?.setObject(listReference, forKey: "list")
17
    }
18
    
19
    // Configure Record

20
    item?.setObject(name, forKey: "name")
21
    
22
    // Show Progress HUD

23
    SVProgressHUD.show()
24
    
25
    // Save Record

26
    privateDatabase.saveRecord(item!) { (record, error) -> Void in
27
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
28
            // Dismiss Progress HUD

29
            SVProgressHUD.dismiss()
30
            
31
            // Process Response

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

The implementation of processResponse(record:error:) contains nothing new. We check to see if any errors popped up and, if no errors were thrown, we notify the delegate.

1
// MARK: -

2
// MARK: Helper Methods

3
private func processResponse(record: CKRecord?, error: NSError?) {
4
    var message = ""
5
    
6
    if let error = error {
7
        print(error)
8
        message = "We were not able to save your item."
9
        
10
    } else if record == nil {
11
        message = "We were not able to save your item."
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
        presentViewController(alertController, animated: true, completion: nil)
20
        
21
    } else {
22
        // Notify Delegate

23
        if newItem {
24
            delegate?.controller(self, didAddItem: item!)
25
        } else {
26
            delegate?.controller(self, didUpdateItem: item!)
27
        }
28
        
29
        // Pop View Controller

30
        navigationController?.popViewControllerAnimated(true)
31
    }
32
}

Step 5: Updating ListViewController

We still have some work to do in the ListViewController class. Start by conforming the ListViewController class to the AddItemViewControllerDelegate protocol. This is also a good moment to declare a constant for the segue with the identifier ItemDetail.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
let SegueItemDetail = "ItemDetail"
6
7
class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddItemViewControllerDelegate {
8
    ...
9
}

Implementing the AddItemViewControllerDelegate protocol is trivial. In controller(_:didAddItem:), we add the new item to items, sort items, reload the table view, and invoke updateView.

1
// MARK: -

2
// MARK: Add Item View Controller Delegate Methods

3
func controller(controller: AddItemViewController, didAddItem item: CKRecord) {
4
    // Add Item to Items

5
    items.append(item)
6
    
7
    // Sort Items

8
    sortItems()
9
    
10
    // Update Table View

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

14
    updateView()
15
}

The implementation of controller(_:didUpdateItem:) is even easier. We sort items and reload the table view.

1
func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) {
2
    // Sort Items

3
    sortItems()
4
    
5
    // Update Table View

6
    tableView.reloadData()
7
}

In sortItems, we sort the array of CKRecord instances by name using the sortInPlace function, a method of the MutableCollectionType protocol.

1
private func sortItems() {
2
    items.sortInPlace {
3
        var result = false
4
        let name0 = $0.objectForKey("name") as? String
5
        let name1 = $1.objectForKey("name") as? String
6
        
7
        if let itemName0 = name0, itemName1 = name1 {
8
            result = itemName0.localizedCaseInsensitiveCompare(itemName1) == .OrderedAscending
9
        }
10
        
11
        return result
12
    }
13
}

There are two more features we need to implement: updating and deleting shopping list items.

Step 6: Deleting Items

To delete items, we need to implement tableView(_:commitEditingStyle:forRowAtIndexPath:) of the UITableViewDataSource protocol. We fetch the shopping list item that needs to be deleted and pass it to the deleteRecord(_:) method.

1
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
2
    guard editingStyle == .Delete else { return }
3
    
4
    // Fetch Record

5
    let item = items[indexPath.row]
6
    
7
    // Delete Record

8
    deleteRecord(item)
9
}

The implementation of deleteRecord(_:) doesn't contain anything new. We invoke deleteRecordWithID(_:completionHandler:) on the private database and process the response in the completion handler.

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

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

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

9
    privateDatabase.deleteRecordWithID(item.recordID) { (recordID, error) -> Void in
10
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
11
            // Dismiss Progress HUD

12
            SVProgressHUD.dismiss()
13
            
14
            // Process Response

15
            self.processResponseForDeleteRequest(item, recordID: recordID, error: error)
16
        })
17
    }
18
}

In processResponseForDeleteRequest(record:recordID:error:), we update the items property and the user interface. If something went wrong, we notify the user by showing an alert.

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

14
        let index = items.indexOf(record)
15
        
16
        if let index = index {
17
            // Update Data Source

18
            items.removeAtIndex(index)
19
            
20
            if items.count > 0 {
21
                // Update Table View

22
                tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Right)
23
                
24
            } else {
25
                // Update Message Label

26
                messageLabel.text = "No Items 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
        presentViewController(alertController, animated: true, completion: nil)
39
    }
40
}

Step 7: Updating Items

The user can update an item by tapping the detail disclosure indicator. This means we need to implement the tableView(_:accessoryButtonTappedForRowWithIndexPath:) delegate method. In this method, we store the user's selection and manually perform the ListDetail segue. Note that nothing happens in the tableView(_:didSelectRowAtIndexPath:) method. All we do is deselect the row the user tapped.

1
// MARK: -

2
// MARK: Table View Delegate Methods

3
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
4
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
5
}
6
7
func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
8
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
9
    
10
    // Save Selection

11
    selection = indexPath.row
12
    
13
    // Perform Segue

14
    performSegueWithIdentifier(SegueItemDetail, sender: self)
15
}

In prepareForSegue(_:sender:), we fetch the shopping list item using the value of the selection property and configure the destination view controller, an instance of the AddItemViewController class.

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 SegueItemDetail:
8
        // Fetch Destination View Controller

9
        let addItemViewController = segue.destinationViewController as! AddItemViewController
10
        
11
        // Configure View Controller

12
        addItemViewController.list = list
13
        addItemViewController.delegate = self
14
        
15
        if let selection = selection {
16
            // Fetch Item

17
            let item = items[selection]
18
            
19
            // Configure View Controller

20
            addItemViewController.item = item
21
        }
22
    default:
23
        break
24
    }
25
}

That's all we need to do to delete and update shopping list items. In the next section, I'll show you how easy it is to update the data model in the CloudKit Dashboard.

4. Updating the Data Model

If you've ever worked with Core Data, then you know that updating the data model should be done with caution. You need to make sure you don't break anything or corrupt any of the application's persistent stores. CloudKit is a bit more flexible.

The Items record type currently has two fields, name and list. I want to show you what it involves to update the data model by adding a new field. Open the CloudKit Dashboard and add a new field to the Items record. Set field name to number and set the field type to Int(64). Don't forget to save your changes.

Update Item Record TypeUpdate Item Record TypeUpdate Item Record Type

Let's now add the ability to modify an item's number. Open AddItemViewController.swift and declare two outlets, a label and a stepper.

1
import UIKit
2
import CloudKit
3
import SVProgressHUD
4
5
protocol AddItemViewControllerDelegate {
6
    func controller(controller: AddItemViewController, didAddItem item: CKRecord)
7
    func controller(controller: AddItemViewController, didUpdateItem item: CKRecord)
8
}
9
10
class AddItemViewController: UIViewController {
11
    
12
    @IBOutlet weak var numberLabel: UILabel!
13
    @IBOutlet weak var numberStepper: UIStepper!
14
    @IBOutlet weak var nameTextField: UITextField!
15
    @IBOutlet weak var saveButton: UIBarButtonItem!
16
    
17
    var delegate: AddItemViewControllerDelegate?
18
    var newItem: Bool = true
19
    
20
    var list: CKRecord!
21
    var item: CKRecord?
22
    
23
    ...
24
    
25
}

We also need to add an action that is triggered when the stepper's value changes. In numberDidChange(_:), we update the contents of numberLabel.

1
@IBAction func numberDidChange(sender: UIStepper) {
2
    let number = Int(sender.value)
3
    
4
    // Update Number Label

5
    numberLabel.text = "\(number)"
6
}

Open Main.storyboard and add a label and a stepper to the add item view controller. Connect the outlets of the view controller to the corresponding user interface elements and connect the numberDidChange(_:) action to the stepper for the Value Changed event.

Update Add Item View ControllerUpdate Add Item View ControllerUpdate Add Item View Controller

The save(_:) action of the AddItemViewController class also changes slightly. Let's see what that looks like.

We only need to add two lines of code. At the top, we store the value of the stepper in a constant, number. When we configure item, we set number as the value for the number key, and that's all there is to it.

1
@IBAction func save(sender: AnyObject) {
2
    // Helpers

3
    let name = nameTextField.text
4
    let number = Int(numberStepper.value)
5
    
6
    // Fetch Private Database

7
    let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase
8
    
9
    if item == nil {
10
        // Create Record

11
        item = CKRecord(recordType: RecordTypeItems)
12
        
13
        // Initialize Reference

14
        let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf)
15
        
16
        // Configure Record

17
        item?.setObject(listReference, forKey: "list")
18
    }
19
    
20
    // Configure Record

21
    item?.setObject(name, forKey: "name")
22
    item?.setObject(number, forKey: "number")
23
    
24
    // Show Progress HUD

25
    SVProgressHUD.show()
26
    
27
    print(item?.recordType)
28
    
29
    // Save Record

30
    privateDatabase.saveRecord(item!) { (record, error) -> Void in
31
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
32
            // Dismiss Progress HUD

33
            SVProgressHUD.dismiss()
34
            
35
            // Process Response

36
            self.processResponse(record, error: error)
37
        })
38
    }
39
}

We also need to implement a helper method to update the user interface of the add item view controller. The updateNumberStepper method checks if the record has a field named number and updates the stepper if it does.

1
private func updateNumberStepper() {
2
    if let number = item?.objectForKey("number") as? Double {
3
        numberStepper.value = number
4
    }
5
}

We invoke updateNumberStepper in the setupView method of the AddItemViewController class.

1
private func setupView() {
2
    updateNameTextField()
3
    updateNumberStepper()
4
    updateSaveButton()
5
}

To visualize the number of each item, we need to make one change to the ListViewController. In tableView(_:cellForRowAtIndexPath:), we set the contents of the cell's right label to the value of the item's number field.

1
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
2
    // Dequeue Reusable Cell

3
    let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath)
4
    
5
    // Configure Cell

6
    cell.accessoryType = .DetailDisclosureButton
7
    
8
    // Fetch Record

9
    let item = items[indexPath.row]
10
    
11
    if let itemName = item.objectForKey("name") as? String {
12
        // Configure Cell

13
        cell.textLabel?.text = itemName
14
        
15
    } else {
16
        cell.textLabel?.text = "-"
17
    }
18
    
19
    if let itemNumber = item.objectForKey("number") as? Int {
20
        // Configure Cell

21
        cell.detailTextLabel?.text = "\(itemNumber)"
22
        
23
    } else {
24
        cell.detailTextLabel?.text = "1"
25
    }
26
    
27
    return cell
28
}

That's all we need to do to implement the changes we made to the data model. There's no need to perform a migration or anything like that. CloudKit takes care of the nitty gritty details.

Conclusion

You should now have a proper foundation in the CloudKit framework. I hope you agree that Apple has done a great job with this framework and the CloudKit Dashboard. There's a lot that we haven't covered in this series, but by now you've learned enough to jump in and get started with CloudKit in your own projects.

If you have any questions or comments, feel free to leave them 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.