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.
import UIKit import CloudKit import SVProgressHUD class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { static let ItemCell = "ItemCell" @IBOutlet weak var messageLabel: UILabel! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView! var list: CKRecord! var items = [CKRecord]() var selection: Int? ... }
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.
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
.
// MARK: - // MARK: View Life Cycle override func viewDidLoad() { super.viewDidLoad() // Set Title title = list.objectForKey("name") as? String setupView() fetchItems() }
The setupView
method is identical to the one we implemented in the ListsViewController
class.
// MARK: - // MARK: View Methods private func setupView() { tableView.hidden = true messageLabel.hidden = true activityIndicatorView.startAnimating() }
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.
private func updateView() { let hasRecords = items.count > 0 tableView.hidden = !hasRecords messageLabel.hidden = hasRecords activityIndicatorView.stopAnimating() }
I'm going to leave fetchItems
empty for now. We'll revisit this method once we're finished setting up the list view controller.
// MARK: - // MARK: Helper Methods private func fetchItems() { }
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.
// MARK: - // MARK: Table View Data Source Methods func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1; } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { // Dequeue Reusable Cell let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath) // Configure Cell cell.accessoryType = .DetailDisclosureButton // Fetch Record let item = items[indexPath.row] if let itemName = item.objectForKey("name") as? String { // Configure Cell cell.textLabel?.text = itemName } else { cell.textLabel?.text = "-" } return cell } func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { return true }
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.
// MARK: - // MARK: Table View Delegate Methods func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) }
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.
// MARK: - // MARK: Segue Life Cycle override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { guard let identifier = segue.identifier else { return } switch identifier { case SegueList: // Fetch Destination View Controller let listViewController = segue.destinationViewController as! ListViewController // Fetch Selection let list = lists[tableView.indexPathForSelectedRow!.row] // Configure View Controller listViewController.list = list case SegueListDetail: ... default: break } }
To satisfy the compiler, we also need to declare the SegueList
constant at the top of ListsViewController.swift.
import UIKit import CloudKit import SVProgressHUD let RecordTypeLists = "Lists" let SegueList = "List" let SegueListDetail = "ListDetail" class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate { ... }
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.
Head back to Xcode, open ListsViewController.swift, and declare a new constant at the top for the Items record type.
import UIKit import CloudKit import SVProgressHUD let RecordTypeLists = "Lists" let RecordTypeItems = "Items" let SegueList = "List" let SegueListDetail = "ListDetail" class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate { ... }
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.
private func fetchItems() { // Fetch Private Database let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase // Initialize Query let reference = CKReference(recordID: list.recordID, action: .DeleteSelf) let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %@", reference)) // Configure Query query.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] // Perform Query privateDatabase.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in // Process Response on Main Thread self.processResponseForQuery(records, error: error) }) } }
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.
private func processResponseForQuery(records: [CKRecord]?, error: NSError?) { var message = "" if let error = error { print(error) message = "Error Fetching Items for List" } else if let records = records { items = records if items.count == 0 { message = "No Items Found" } } else { message = "No Items Found" } if message.isEmpty { tableView.reloadData() } else { messageLabel.text = message } updateView() }
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.
import UIKit import CloudKit import SVProgressHUD protocol AddItemViewControllerDelegate { func controller(controller: AddItemViewController, didAddItem item: CKRecord) func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) } class AddItemViewController: UIViewController { @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var saveButton: UIBarButtonItem! var delegate: AddItemViewControllerDelegate? var newItem: Bool = true var list: CKRecord! var item: CKRecord? ... }
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.
// MARK: - // MARK: Actions @IBAction func cancel(sender: AnyObject) { navigationController?.popViewControllerAnimated(true) } @IBAction func save(sender: AnyObject) { navigationController?.popViewControllerAnimated(true) }
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.
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 nil
, newItem
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.
// MARK: - // MARK: View Life Cycle override func viewDidLoad() { super.viewDidLoad() setupView() // Update Helper newItem = item == nil // Add Observer let notificationCenter = NSNotificationCenter.defaultCenter() notificationCenter.addObserver(self, selector: "textFieldTextDidChange:", name: UITextFieldTextDidChangeNotification, object: nameTextField) }
In viewDidAppear(animated:)
, we show the keyboard by calling becomeFirstResponder
on nameTextField
.
override func viewDidAppear(animated: Bool) { nameTextField.becomeFirstResponder() }
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.
// MARK: - // MARK: View Methods private func setupView() { updateNameTextField() updateSaveButton() } // MARK: - private func updateNameTextField() { if let name = item?.objectForKey("name") as? String { nameTextField.text = name } } // MARK: - private func updateSaveButton() { let text = nameTextField.text if let name = text { saveButton.enabled = !name.isEmpty } else { saveButton.enabled = false } }
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.
// MARK: - // MARK: Notification Handling func textFieldTextDidChange(notification: NSNotification) { updateSaveButton() }
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.
@IBAction func save(sender: AnyObject) { // Helpers let name = nameTextField.text // Fetch Private Database let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase if item == nil { // Create Record item = CKRecord(recordType: RecordTypeItems) // Initialize Reference let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf) // Configure Record item?.setObject(listReference, forKey: "list") } // Configure Record item?.setObject(name, forKey: "name") // Show Progress HUD SVProgressHUD.show() // Save Record privateDatabase.saveRecord(item!) { (record, error) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in // Dismiss Progress HUD SVProgressHUD.dismiss() // Process Response self.processResponse(record, error: error) }) } }
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.
// MARK: - // MARK: Helper Methods private func processResponse(record: CKRecord?, error: NSError?) { var message = "" if let error = error { print(error) message = "We were not able to save your item." } else if record == nil { message = "We were not able to save your item." } if !message.isEmpty { // Initialize Alert Controller let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .Alert) // Present Alert Controller presentViewController(alertController, animated: true, completion: nil) } else { // Notify Delegate if newItem { delegate?.controller(self, didAddItem: item!) } else { delegate?.controller(self, didUpdateItem: item!) } // Pop View Controller navigationController?.popViewControllerAnimated(true) } }
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.
import UIKit import CloudKit import SVProgressHUD let SegueItemDetail = "ItemDetail" class ListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddItemViewControllerDelegate { ... }
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
.
// MARK: - // MARK: Add Item View Controller Delegate Methods func controller(controller: AddItemViewController, didAddItem item: CKRecord) { // Add Item to Items items.append(item) // Sort Items sortItems() // Update Table View tableView.reloadData() // Update View updateView() }
The implementation of controller(_:didUpdateItem:)
is even easier. We sort items
and reload the table view.
func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) { // Sort Items sortItems() // Update Table View tableView.reloadData() }
In sortItems
, we sort the array of CKRecord
instances by name using the sortInPlace
function, a method of the MutableCollectionType
protocol.
private func sortItems() { items.sortInPlace { var result = false let name0 = $0.objectForKey("name") as? String let name1 = $1.objectForKey("name") as? String if let itemName0 = name0, itemName1 = name1 { result = itemName0.localizedCaseInsensitiveCompare(itemName1) == .OrderedAscending } return result } }
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.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { guard editingStyle == .Delete else { return } // Fetch Record let item = items[indexPath.row] // Delete Record deleteRecord(item) }
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.
private func deleteRecord(item: CKRecord) { // Fetch Private Database let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase // Show Progress HUD SVProgressHUD.show() // Delete List privateDatabase.deleteRecordWithID(item.recordID) { (recordID, error) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in // Dismiss Progress HUD SVProgressHUD.dismiss() // Process Response self.processResponseForDeleteRequest(item, recordID: recordID, error: error) }) } }
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.
private func processResponseForDeleteRequest(record: CKRecord, recordID: CKRecordID?, error: NSError?) { var message = "" if let error = error { print(error) message = "We are unable to delete the item." } else if recordID == nil { message = "We are unable to delete the item." } if message.isEmpty { // Calculate Row Index let index = items.indexOf(record) if let index = index { // Update Data Source items.removeAtIndex(index) if items.count > 0 { // Update Table View tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Right) } else { // Update Message Label messageLabel.text = "No Items Found" // Update View updateView() } } } else { // Initialize Alert Controller let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .Alert) // Present Alert Controller presentViewController(alertController, animated: true, completion: nil) } }
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.
// MARK: - // MARK: Table View Delegate Methods func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) } func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) { tableView.deselectRowAtIndexPath(indexPath, animated: true) // Save Selection selection = indexPath.row // Perform Segue performSegueWithIdentifier(SegueItemDetail, sender: self) }
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.
// MARK: - // MARK: Segue Life Cycle override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { guard let identifier = segue.identifier else { return } switch identifier { case SegueItemDetail: // Fetch Destination View Controller let addItemViewController = segue.destinationViewController as! AddItemViewController // Configure View Controller addItemViewController.list = list addItemViewController.delegate = self if let selection = selection { // Fetch Item let item = items[selection] // Configure View Controller addItemViewController.item = item } default: break } }
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 the field type to Int(64). Don't forget to save your changes.
Let's now add the ability to modify an item's number. Open AddItemViewController.swift and declare two outlets, a label and a stepper.
import UIKit import CloudKit import SVProgressHUD protocol AddItemViewControllerDelegate { func controller(controller: AddItemViewController, didAddItem item: CKRecord) func controller(controller: AddItemViewController, didUpdateItem item: CKRecord) } class AddItemViewController: UIViewController { @IBOutlet weak var numberLabel: UILabel! @IBOutlet weak var numberStepper: UIStepper! @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var saveButton: UIBarButtonItem! var delegate: AddItemViewControllerDelegate? var newItem: Bool = true var list: CKRecord! var item: CKRecord? ... }
We also need to add an action that is triggered when the stepper's value changes. In numberDidChange(_:)
, we update the contents of numberLabel
.
@IBAction func numberDidChange(sender: UIStepper) { let number = Int(sender.value) // Update Number Label numberLabel.text = "\(number)" }
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.
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.
@IBAction func save(sender: AnyObject) { // Helpers let name = nameTextField.text let number = Int(numberStepper.value) // Fetch Private Database let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase if item == nil { // Create Record item = CKRecord(recordType: RecordTypeItems) // Initialize Reference let listReference = CKReference(recordID: list.recordID, action: .DeleteSelf) // Configure Record item?.setObject(listReference, forKey: "list") } // Configure Record item?.setObject(name, forKey: "name") item?.setObject(number, forKey: "number") // Show Progress HUD SVProgressHUD.show() print(item?.recordType) // Save Record privateDatabase.saveRecord(item!) { (record, error) -> Void in dispatch_async(dispatch_get_main_queue(), { () -> Void in // Dismiss Progress HUD SVProgressHUD.dismiss() // Process Response self.processResponse(record, error: error) }) } }
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.
private func updateNumberStepper() { if let number = item?.objectForKey("number") as? Double { numberStepper.value = number } }
We invoke updateNumberStepper
in the setupView
method of the AddItemViewController
class.
private func setupView() { updateNameTextField() updateNumberStepper() updateSaveButton() }
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.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { // Dequeue Reusable Cell let cell = tableView.dequeueReusableCellWithIdentifier(ListViewController.ItemCell, forIndexPath: indexPath) // Configure Cell cell.accessoryType = .DetailDisclosureButton // Fetch Record let item = items[indexPath.row] if let itemName = item.objectForKey("name") as? String { // Configure Cell cell.textLabel?.text = itemName } else { cell.textLabel?.text = "-" } if let itemNumber = item.objectForKey("number") as? Int { // Configure Cell cell.detailTextLabel?.text = "\(itemNumber)" } else { cell.detailTextLabel?.text = "1" } return cell }
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.