Chapter 7 discussed storing preferences both locally on the AppleTV and in the cloud using NSUserDefaults and NSUbiquitousKeyValueStore. This method works great for storing small pieces of information, but what happens when the app needs to store a significant amount of information? What happens when an app needs to search or sort this type of information? This is where CloudKit comes in. CloudKit is a framework provided by Apple that allows developers to easily sync databases between different devices.
CloudKit is currently available only for iOS , Mac OS X, and tvOS devices. Apple has provided CloudKit JS. CloudKit JS looks to enable web apps and any other apps that can implement javascript to hook into existing CloudKit databases. This chapter will not cover CloudKit JS since it is not needed on tvOS.
Considerations for Using CloudKit
CloudKit is virtually free! Apple currently provides developers with 10GB of asset storage and 2GB data transfer per month for free with a developer account. They also provide 100MB of database storage and 40 requests per second. All of this storage is provided for free, but that is not the best part. Apple increases all four of the limitations as you add additional users. For example, with 100,000 active users, the asset storage is increased to 25TB. As an app scales even larger, Apple will provide up to 1 petabyte (PB or 1000 terabytes or 1,000,000 gigabytes) of asset storage and 400 requests per second. Most apps will be able to implement CloudKit without paying any monthly fees. Apple provides pricing and limits details on their iCloud for Developers site (https://developer.apple.com/icloud/).
CloudKit data are either stored as public or private. Public data are available to all of those with the app. Private data are available only to specific iCloud accounts. As a developer designs their app, they need to consider what types of data are available to all and which data are account specific.
CloudKit Containers
All CloudKit data are separated into different containers. Containers will hold all of the databases and information for each CloudKit-enabled app. Each app will have its own container. It is, however, possible to share containers with different apps from the same developer. The container ID will match the app’s bundle ID. Apple provides a class called CKContainer for accessing the different containers.
The default app container can be accessed by using the following code:
var myContainer = CKContainer.defaultContainer()
Creating a custom container is easy.
Figure 8-1. Select iCloud Containers
Figure 8-2. Adding a new iCloud Container
Figure 8-3. Creating a new Container
Figure 8-4. Finished Creating the CloudKit Container
Click the Register button and the container is now ready to be used. To access this container, use the following code:
myContainer = CKContainer.init("iCloud.com.inno.cloudkit2")
Databases
The container ID will need to be replaced with whatever you used to register your container in the previous step. Each container will contain a public and a private database. The public database will contain information and assets that are shared among all of the instances of the database. All users will have read and write access to the public database through your app.
Note A developer needs to be careful when dealing with the public database. If one user is able to delete items from it, all other users will be affected.
The private database is only accessible to the current user. The user will have to enter their username and password, but then they will have read and write access to that database.
CloudKit Databases
Apple has provided the CKDatabase class for accessing the databases. Once a container has been connected, it is easy to access the public and private databases. Use the following code to access the private database:
var myContainer = CKContainer.defaultContainer()
var publicDatabase: CKDatabase = myContainer.publicCloudDatabase
var privateDatabase: CKDatabase = myContainer.privateCloudDatabase
A CKRecord is used to store data in your database. Each CKRecord has different pieces of data stored as key-value pairs. Records should be separated into distinct tables or record types for each type of data. For example, for a bookstore, it would make sense to have a record type of Book and a separate record type of Author. Each record type will have a name and fields associated with it. Table 8-1 from Apple’s documentation (https://developer.apple.com/library/tvos/documentation/DataManagement/Conceptual/CloudKitQuickStart/CreatingaSchemabySavingRecords/CreatingaSchemabySavingRecords.html) shows the possible field types for a record.
Table 8-1. Possible Field Types for a Record
Field Type |
Class |
Description |
---|---|---|
Asset |
CKAsset |
A large file that is associated with a record but stored separately |
Bytes |
NSData |
A wrapper for byte buffers that is stored with the record |
Date/Time |
NSDate |
A single point in time |
Double |
NSNumber |
A double |
Int(64) |
NSNumber |
An integer |
Location |
CLLocation |
A geographical coordinate and altitude |
Reference |
CKReference |
A relationship from one object to another |
String |
NSString |
An immutable text string |
List |
NSArray |
Arrays of any of the above field types |
Creating a record is a fairly easy process. The following code would accomplish this:
let newBook = CKRecord(recordType: "Book")
This creates a new constant called newBook that is of the type Book. Now, you are able to set values of fields on this newBook constant:
newBook.setValue("The Hobbit", forKey: "title")
newBook.setValue("J. R. R. Tolkien", forKey: "author")
This code sets the title of the book equal to "The Hobbit". The author of the book was also set. Now that the CKRecord of the book is all set, it can be saved to the database. You would save this record to the public database in this case with the following code:
27 CKContainer.defaultContainer().publicCloudDatabase ().saveRecord(newBook, completionHandler: { (record: CKRecord?, error: NSError?) in
28
29 if error != nil {
30 print("There was an error")
31
32 } else {
33 print("Record Saved Successfully")
34 }
35 })
Line 27 tells the public database of the default container to save this record. It also passes in a completion handler, which is a method that will run when the first method is complete. The completion method, in this case, merely tells you if there was an issue saving the record.
Let’s create a CloudKit tvOS app. Launch Xcode and select Create a new Xcode project. You will be prompted with the type of project. Select tvOS on the left-hand side and select Single View Application, as shown in Figure 8-5. Click Next.
Figure 8-5. Create a Single View tvOS application
On the next screen, fill in the name of your application. We are using CKBookStore for the name. The Organization Name and Organization Identifier should already be filled in. Make sure the language selected is Swift. None of the check boxes need to be checked (see Figure 8-6). Then click Next.
Figure 8-6. Naming the project
You will be prompted to save your project. Select a location you can easily access. Once Xcode opens the project, you will see something similar to Figure 8-7.
Figure 8-7. New project screen
By default, the project is selected in the Project Navigator on the left, and the active target will be selected in the targets list. This allows you to change settings and enable CloudKit as the active target. Select Capabilities from the Targets menu, as shown in Figure 8-8.
Figure 8-8. Select Capabilities in the Targets menu
Expand the arrow next to iCloud. Toggle the switch to On. If your user account belongs to multiple teams, Xcode will prompt you to select the team to connect with this application. Make sure CloudKit is checked under the Services heading. Your screen should now look like Figure 8-9. The Container IDs
Figure 8-9. Successfully added CloudKit to the app
For this app, the default container will be used. If additional containers are available for this app, they will be displayed and are able to be selected on this screen. Apple also provides a link to the CloudKit Dashboard. This is a web interface Apple provides developers for administering CloudKit databases. Click the CloudKit Dashboard.
After entering your developer credentials, you should see a screen similar to Figure 8-10.
Figure 8-10. CloudKit Dashboard
You won’t be doing anything in the Dashboard for now, but it is useful for managing record types, queries, and records. You will be using it later in the chapter.
For now, let’s go back to the app and click the AppDelegate.swift file in the Project Navigator. You need to add a method to create book records in CloudKit. Under the import UIKit line, add the following line:
import CloudKit
Now add the following method at the end of the file, but inside the closing brace:
45 func setupBooks() {
46
47 let newBook = CKRecord(recordType: "Books")
48 newBook["title"] = "The Hobbit"
49 newBook["author"] = "J. R. R. Tolkien"
50
51 CKContainer.defaultContainer().publicCloudDatabase.saveRecord(newBook) { (record: CKRecord?, error: NSError?) -> Void in
52 print("Done")
53 if(error != nil) {
54 print("error")
55 print(error.debugDescription)
56 }
57 }
58 }
Line 45 creates the method called setupBooks. Line 47 creates a new CKRecord of the type Book. Lines 48 and 49 add a title and an author to this book. Line 51 is a little more complicated. It starts by telling the default container to tell its public database to save this record. You then pass in a block method to be executed when the save is either completed or fails. You are passing two parameters. The first one is the record you tried to save and the second one is the error, if any. Line 53 checks to see if there was an error and displays the information about it.
Note If the save function fails, many times it is because the user is not logged into their iCloud account on the device.
To call this method, add the following line to the application didFinishLaunchingWithOptions at the beginning of the file:
setupBooks()
This will add a book to your cloud every time the app is launched. This is a good way to test things out, but you will definitely want to change this in the real world. Once done, your AppDelegate.swift file should look like the one shown in Figure 8-11.
Figure 8-11. Finished AppDelegate
Run your app and see if the record was successfully saved. It should be. Open the Console log in Xcode to verify the word “Done.” If you do see an error in the log, it is likely the user will need to log into iCloud on the device.
Now that you have saved this book, you need to work on getting all of the book records from the cloud. You will retrieve the cloud information in the ViewController.swift. Click ViewController.swift. Add the import CloudKit line at the top of the file like we did in the AppDelegate.swift file. In the viewDidLoad method, add the following code to the bottom of the method:
20 let myPredicate: NSPredicate = NSPredicate(value: true)
21 let myQuery: CKQuery = CKQuery(recordType: "Books", predicate: myPredicate)
22
23 CKContainer.defaultContainer().publicCloudDatabase.performQuery(myQuery, inZoneWithID: nil) {
24 results, error in
25 if error != nil {
26 print("Error")
27 print(error.debugDescription)
28 } else {
29 print(results)
30 }
31 }
Line 20 creates an NSPredicate, which is used to create a search query. NSPredicates are also used with Core Data. They are a powerful way to query. The NSPredicate only queries the records where value=true, and this is how you query all of the records. True is always true, so this will create an NSPredicate to return all of the records.
Line 21 creates a CKQuery by passing in the record type and the NSPredicate you created in the previous line. A CKQuery can also have an NSSortDescriptor. This allows you to sort the data you are retrieving back from CloudKit.
Line 23 tells the public database to perform the query. It is possible to segregate your records into different zones. That is beyond the scope of this book, so here just send in nil to the zone identifier parameter.
Lines 24 to 33 are the block methods to be executed once the query is complete. You can now check to see if there is an error. If something failed, it will then display the error in the log. If there is no error, you can print the records you received into the log. Once complete, your code should look like that shown in Figure 8-12.
Figure 8-12. Finished viewDidLoad
If you now run this app as it stands, you will receive an error. You now need to go to the CloudKit Dashboard located at https://icloud.developer.apple.com/dashboard/.
One the left-hand side, click Record Types, then Books, as shown in Figure 8-13. The number of public records will change depending on the number of times you have run the app.
Figure 8-13. Selecting Books Record Type
You will now see a screen similar to that shown in Figure 8-14.
Figure 8-14. Books details
Click the downward arrow underneath Metadata Indexes and check the box next to Record ID, as shown in Figure 8-15. This allows your application to access these metadata as part of a query. You will notice, Apple will inform you of the cost of selecting that index. It will add 5% to your storage requirements. This is fine in this case, but when designing for large CloudKit applications, size will need to be considered.
Figure 8-15. Creating a Record ID index
Now click the Save button at the bottom right corner of the screen. Launch your app and you should receive a log similar to that shown in Figure 8-16. There will be one line for each time you called setupBooks().
Figure 8-16. Retrieving records from CloudKit
Summary
In this chapter you learned about CloudKit and the basic objects required to access CloudKit. You learned about the CloudKit Developer Console and how to view records and record types in a web browser. You also created an app to save CloudKit records and retrieve them.
This book as shown you how to begin development for the new AppleTV. We have shown some of the familiar iOS controls and classes and also highlighted some of the ones that are different for tvOS. Due to the tvOS lack of local storage, we also spent time demonstrating how to store and retrieve data from iCloud.
Exercises