Building a Shopping List Application With CloudKit: Adding Relationships

In the previous tutorial 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 7 and Swift 2. 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 second tutorial 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.

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 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 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, 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 Controller

Step 3: Configuring 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.

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

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.

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

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.

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.

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.

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

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 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 field name to name and set 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 field name to list and set field type to Reference. The Reference field type was designed for this exact purpose, managing relationships.

Item Record Type

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

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.

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.

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

3. Adding Items

Step 1: Creating AddItemViewControler

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.

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.

Step 2: Creating 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 Controller

Step 3: Configuring 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 changes.

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

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.

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.

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

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.

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 identifier ItemDetail.

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.

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

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

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.

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.

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.

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.

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.

That's all we need to do to delete and update shopping list items. In the next section, we explore 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 field type to Int(64). Don't forget to save your changes.

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

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

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

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.

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

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.

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 of the CloudKit framework. I hope you agree that Apple has done a great job with the framework and the CloudKit Dashboard. There's a lot that we haven't covered in this series, but this should give you enough to 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.

Tags:

Comments

Related Articles