© Radoslava Leseva Adams and Hristo Lesev 2016

Radoslava Leseva Adams and Hristo Lesev, Migrating to Swift from Flash and ActionScript, 10.1007/978-1-4842-1666-8_13

13. Working with Data

Radoslava Leseva Adams and Hristo Lesev2

(1)London, UK

(2)Kazanlak, Bulgaria

Working with data is a big part of app development. Anything from storing user preferences and achievements to providing larger storage for the users’ artistic creations requires that you know how to work with data, how to store it locally, and how to offer your users the option to back it up in the cloud.

This chapter is split into two parts. In the first part we explore different ways of persisting local data on a device. The second part introduces iCloud—Apple’s cloud solution. We will see how to set up an account for it and use it to store data remotely.

Reading and Writing Data Locally

The iOS SDK offers several different ways of persisting data locally on the device’s file system. The examples that follow will help explore them: we will save and read arbitrary data from a text file, then we will see how to serialize objects into files and into a key-value database.

The iOS File System

Let us first have a look at how iOS manages files internally. As there can be lots of applications installed on a device and trying to read and write data to it, there are restrictions on which parts of the file system an application can access.

Every application has its own sandbox, which defines the disc locations that are available to it and is not accessible to other apps. The two directories where an app can store files and manage folders are called Documents and tmp. The Documents directory is where user-generated content goes—it persists between launches of the app. The contents of this folder can be made available to the user through file sharing and are backed up by iTunes. The tmp folder is used, as its name suggests, for storing temporary information that doesn’t need to be kept between application launches.

There are three important classes that the iOS SDK offers for working with files and folders:

  • NSFileManager is used to perform basic file and folder operations such as creating, moving, writing, and reading. It can be queried for the current working folder and offers folder management: creating and deleting folders and enumerating their contents.

  • NSFileHandle is helpful with low-level file operations such as seeking a position in a file, appending new data, or reading and writing specific chunks of data from a file.

  • NSData is used as a storage buffer: for example, it can store the contents of a file in memory.

Note

Path names in iOS use the standard UNIX convention, where each path component is separated with a forward slash /.

The applications you develop are installed in /private/var/mobile/Containers/Data/Application/YourAppUniqueIDString.

In the next section we will use the three classes we mentioned previously to create an application which stores a text file in the Documents folder and reads it back.

Preparation: Setting Up the App

Our app will initially save a text file, which it will then read back. Later on in the chapter we will add more advanced functionality to it.

Start by creating a new Single View Application project in Xcode (FileNewProject…, then iOSApplicationSingle View Application), named LocalStorageDemo. The app will have a very simple user interface (UI), consisting of a UITextView and two buttons: find these in the Object library and drag them onto the storyboard in Main.storyboard to make it look as shown on Figure 13-1. Change the button captions to read Open and Save and leave the text in the text view as it is.

A371202_1_En_13_Fig1_HTML.jpg
Figure 13-1. LocalStorageDemo Application user interface

Ctrl+drag the text view into the ViewController class definition in the ViewController.swift file to create an outlet for it, named m_textView. Then Ctrl+drag each of the buttons into ViewController to add handlers for their Touch Up Inside actions: name the actions openTextFile and saveTextFile, respectively.

Tip

See Chapter 2 if you need a reminder for how to create UI outlets and event handlers.

After the outlet and the two event handlers have been generated, the code in ViewController.swift should look like that in Listing 13-1.

Listing 13-1. ViewController.swift with an Outlet and Event Handlers for the UI
import UIKit

class ViewController: UIViewController {

    //Outlet for accessing the label and the date picker in Main.storyboard:
    @IBOutlet weak var textView: UITextView!


    @IBAction func openTextFile(sender: AnyObject) {
        //This method is called when the Open button is touched
    }


    @IBAction func saveTextFile(sender: AnyObject) {
        //This method is called when the Save button is touched
    }


    //The rest of the code goes here

}

Hit the run button to make sure that the app compiles and executes, although it doesn’t do much at this point.

Working with File Paths

We will use the NSFileManager class from the iOS SDK to help with the reading and the writing of files. For that purpose we need a reference to it in ViewController: declare a property of the ViewController class, named fileManager, and initialize it with the default file manager by calling the defaultManager method of the NSFileManager class. Then add a property of type String? to ViewController and name it localFilePath. We will use it to store the path to the file we write to and read from (Listing 13-2).

Listing 13-2. Obtaining a Reference of NSFileManager
//Obtain a reference of NSFileManager
let fileManager : NSFileManager = NSFileManager.defaultManager()


//A property to store the path to the file in the file system
var localFilePath : String?

The reading and writing will happen in a text file, called test.txt: let us initialize localFilePath with the path to test.txt right after the view has been shown on screen (in the viewDidLoad method of the ViewController class). We will store test.txt in the tmp folder of our application, but we don’t need to remember or hard-code the full path to it: the NSTemporaryDirectory global function will help us with that. The NSURL class from the iOS SDK can then help us construct the full path to our file.

You can see this done on Listing 13-3: we create a constant, named tmpDirPath, of type NSURL and initialize it with the result from NSTemporaryDirectory. The next constant we create, fileURL, contains the full path to test.txt. Calling fileURL.path gives us the path to our file as a String.

Listing 13-3. Set the Path to the test.txt File
override func viewDidLoad() {
    super.viewDidLoad()


    //Get a URL path to the Application's tmp directory
    let tmpDirPath = NSURL(fileURLWithPath: NSTemporaryDirectory())


    //Append a file name to the path
    let fileURL = tmpDirPath.URLByAppendingPathComponent("test.txt")


    //Get the file path as string
    localFilePath = fileURL.path


    //Output the resulting path to the console
    print( localFilePath )
}

Saving a Text File

We will add code to the saveTextFile action handler (see Listing 13-4), which will be executed when we tap the Save button. It will save the contents of the text view to test.txt. We will use the reference to NSFileManager we created earlier to do the file writing operation by calling its createFileAtPath method. This method kills two birds with one stone: it creates the file and writes bytes to it. There are a couple of things to note about createFileAtPath:

  • It takes data in the form of NSData: this is another iOS SDK class, which serves as a byte buffer. In order to read and write text to it we will use UTF8 encoding.

  • It returns a Boolean result, which indicates whether writing to the file was successful. This is quite useful, as a lot of things can go wrong during a file-write operation: we can run out of disc space, the path to the file might be incorrect, and so on. We will check the result of createFileAtPath and will show it to the user in an alert: this is done in the showMessage method we will implement later.

Listing 13-4. Save a Text File
@IBAction func saveTextFile(sender: AnyObject) {

    //Get the text from the text view
    //and encode it as NSData with UTF8 encoding
    let fileData : NSData! =
         (textView.text as String).dataUsingEncoding(NSUTF8StringEncoding)


    //Try to create test.txt and add to it the contents of fileData
    let isOk = fileManager.createFileAtPath(localFilePath!,
         contents: fileData, attributes: nil)


    //Test if file creation was successful and show an alert
    if isOk {
        showMessage( message: "File creation successful!" )
    }
    else {
        showMessage( message: "File creation unsuccessful!" )
    }


    //Print to the console for convenience
    print( isOk )
}

Next comes the implementation of the showMessage method (see Listing 13-5): it may look familiar from the example in Chapter 5 .

Listing 13-5. Implementation of the showMessage Method
func showMessage(message message: String) {
    //Instantiate an allert controller
    let alertController = UIAlertController(title: "Local Storage Demo",
        message: message, preferredStyle: UIAlertControllerStyle.Alert)


    //Add a "Dismiss" button to the alert
    alertController.addAction(UIAlertAction(title: "Dismiss",
        style: UIAlertActionStyle.Default,handler: nil))


    //Show the alert on the screen
    self.presentViewController(alertController, animated: true,
        completion: nil)
}

Run the app, edit the text in the text view, and tap Save. You should see the “File creation successful!” alert. Next, we will read the file back.

Reading a Text File

We will implement the file reading in the openTextFile action handler we added when we started the chapter: it will be called when the user taps the Open button. Listing 13-6 shows the implementation: we start clean by removing the current text from the text view: this way we can be confident that what we write to it next is the content of the file on disc and not what was previously in the view. Next we make sure that the file exists by calling NSFileManager’s fileExistsAtPath method. If test.txt is where we expect it to be, we can proceed with the reading: another method of NSFileManager helps us with that—contentsAtPath. You may remember that to write it to the file, we had to convert the String from the text view into NSData (similar to ByteArray in ActionScript). Here we will do the reverse and convert the NSData obtained from calling contentsAtPath back to String before displaying it in the text view.

Listing 13-6. Reading from a Text File
@IBAction func openTextFile(sender: AnyObject) {
    //Clear the text view content for convenience
    textView.text = ""


    //Check if the file exists
    if fileManager.fileExistsAtPath(localFilePath!) {
        //Read the contents of the file as NSData
        let dataBuffer = fileManager.contentsAtPath(localFilePath!)


        //Decode from UTF8 NSData back to NSString
        let dataString = NSString(data: dataBuffer!,
            encoding: NSUTF8StringEncoding)


        //Put the decoded string in the text view
        textView.text = dataString as! String
    }
    else {
        showMessage( message: "File does not exist!" )
    }
}

This was easy, wasn’t it? You now have an app that utilizes NSFileManager to read and write files. There aren’t many limits to the contents of files on iOS: you can store text, images, binary blobs, and so on—anything that can be converted to bytes and put into NSData.

Serializing Objects with NSCoder

One of the types of data you may want to store is the current state of your app: serializing the objects that are in play at a given moment is a convenient way of doing it. The NSCoding protocol form the iOS SDK can help with this: it allows classes to serialize and deserialize themselves into data, which can be stored on the device or transferred across a network.

To try this out we will add a new class to the app we just made and have it conform to the NSCoding protocol. From Xcode’s main menu select FileNewFile and click Swift file to add a new file to the project. Name the file UserSettings.swift and click its name in Project navigator to open it. Add the UserSettings class definition from Listing 13-7 to the file. UserSettings will hold some user information: their name, their age, do they like pizza, and a list of school nicknames—never miss an opportunity for embarrassment.

Listing 13-7. UserSettings Class
class UserSettings : NSObject, NSCoding {
    var userName : String
    var lovesPizza : Bool
    var age: Int
    var nickNames: [String]


    // Memberwise initializer
    init(userName: String, lovesPizza: Bool, age: Int, nickNames: [String]) {
        self.userName = userName
        self.lovesPizza = lovesPizza
        self.age = age
        self.nickNames = nickNames
    }


    //Print all properties of the class to a string
    override var description: String {
            return "UserSettings: (userName), (lovesPizza),
                (age), (nickNames)"
    }


    // MARK: NSCoding
}

You will see that we have defined UserSettingsto conform to the NSCoding protocol. For that purpose it will need to implement two methods, initWithCoder and encodeWithCoder, which will be called when we want to serialize or deserialize instances of UserSettings. Let us add their implementations below the // MARK : NSCoding line.

encodeWithCoder takes an instance of NSCoder: this class offers convenience methods for serializing different types of data and their names as key-value pairs. You can see it in use for storing the user’s age as an integer, the user’s love for pizza as a Bool, and his or her name and the array of nicknames as generic objects. Copy the implementation from Listing 13-8 to the UserSettings file.

Listing 13-8. Serializing of the UserSettings Class
func encodeWithCoder(coder: NSCoder) {
    coder.encodeObject(self.userName, forKey: "userName")
    coder.encodeBool(self.lovesPizza, forKey: "lovesPizza")
    coder.encodeInt(Int32(self.age), forKey: "age")
    coder.encodeObject(self.nickNames, forKey: "nickNames")
}

The deserialization of UserSettings will be done in an initializer method: it too takes an NSCoder instance as an argument. To read the data back, we will call NSCoder’s decodeObjectForKey method. Note that this method returns an NSObject instance, which we will need to convert to the type we expect a given piece of data to be. You can see the implementation of the initializer in Listing 13-9.

Listing 13-9. Deserializing of the UserSettings class
required convenience init?(coder decoder: NSCoder) {
    guard let userName = decoder.decodeObjectForKey("userName") as? String
    else {
        return nil
    }


    let lovesPizza = decoder.decodeBoolForKey("lovesPizza");

    let age = decoder.decodeIntegerForKey("age");

    guard let nickNames = decoder.decodeObjectForKey("nickNames") as?
    [String] else {
        return nil
    }


    self.init(
        userName: userName,
        lovesPizza: lovesPizza,
        age: age,
        nickNames: nickNames
    )
}

To test serializing and deserializing UserSettings (see Listing 13-10), let us create an instance of it in the viewDidLoad method of the ViewController class in ViewController.swift. We will store the instance’s state in an XML file, called userSettings.xml and located in the app’s tmp folder: this utilizes the NSKeyedArchiver class from the iOS SDK: it is a descendant of NSCoder and has convenience methods for storing and retrieving key-value pairs of information.

We will use NSKeyedArchiverfor reading back the serialized object, after which we will output what we read into Xcode’s console. Note that NSKeyedUnarchiver.unarchiveObjectWithFile: returns an NSObject, which we will need to cast to UserSettings.

Listing 13-10. Serializing and Deserializing of a UserSettings Instance
override func viewDidLoad() {
    ...
    //The rest of the method's implementation
    ...


    //Create an instance of the UserSettings class
    let userSettings = UserSettings(userName: "Julius Marx",
                                    lovesPizza: true,
                                    age: 32,
                                    nickNames: ["Groucho"])


    //A file path and name where the userSettings state will be stored
    let archivePath =
        (tmpDirPath.URLByAppendingPathComponent("userSettings.xml")).path


    //Archive userSettings to a file
    NSKeyedArchiver.archiveRootObject(userSettings, toFile: archivePath!)


    //Unarchive the user settings from the file
    let unarchivedSettings : UserSettings =
        NSKeyedUnarchiver.unarchiveObjectWithFile(archivePath!) as! UserSettings


    print(unarchivedSettings)
}

If you now run the app in debug mode you should see the following string printed in the console output window:

Julius Marx, true, 32, ["Groucho"]

Persisting Data with NSUserDefaults

As usual in programming there is more than one way to skin a cat. Each iOS app has its own key-value database for storing simple data such as user preferences. The access to this database is managed by the NSUserDefaults class.

Listing 13-11 shows a modification of the code we added to viewDidLoad in ViewController: instead of writing to an XML file, it persists the state of the UserSettings object into the app’s database. We get hold of the database by calling NSUserDefaults.standardUserDefaults and store the data under a key, which we will name my_userSettings. Storing data for a given key is achieved by calling one of the convenience methods: objectForKey, integerForKey, stringForKey, and so on. Reading data back can be done with one of the set*:forKey methods: setObject:forKey, setInteger:forKey, and so forth.

Listing 13-11. Working with the User Defaults Database
//Create an instance of the UserSettings class
let userSettings = UserSettings(userName: "Julius Marx",
                                lovesPizza: true,
                                age: 32,
                                nickNames: ["Groucho"])


//Serialize userSettings to NSData constant
let settingsData = NSKeyedArchiver.archivedDataWithRootObject(userSettings)


//Put data in the User Defaults database under the key "my_userSettings"
NSUserDefaults.standardUserDefaults().setObject(settingsData,
        forKey: "my_userSettings")


//Retrieve data from the User Defaults database
//associated with "my_userSettings" key
if let unarchivedData =
    NSUserDefaults.standardUserDefaults().objectForKey("my_userSettings") as?
    NSData {
    let unarchivedSettings =
        NSKeyedUnarchiver.unarchiveObjectWithData(unarchivedData)
    print(unarchivedSettings)
}

Dealing with Larger Data: A Word About CoreData

This chapter will not be complete if we do not mention CoreData. It is an object graph persistence framework, which is so powerful that it deserves a book of its own.

Why and where might you find CoreData useful? If you are working on a massive project with many sources of data and complex class hierarchies, CoreData gives you the ability to model relationships between data entities, manage their life cycles, persist them to XML files, and even do SQL queries on them.

Working with the Cloud

Storing data in the cloud gives great power to developers. You can have your data available on multiple devices anywhere around the globe without the need to write a single line of server code.

To help developers embrace cloud technology Apple introduced CloudKit, a framework that directly interacts with Apple’s iCloud servers. CloudKit provides a flexible application programming interface (API) and a dashboard that offers developers access to the data stored on Apple’s iCloud servers. Over the next few pages we will discuss the basics of the CloudKit framework and will make a shopping list app that stores its data in the cloud.

Note

Using iCloud requires that you enroll in Apple’s iOS Development Program ( https://goo.gl/iDkemP ). This is a paid membership program, which you can add to your account on Apple’s web site.

CloudKit Basics

Let us first observe how the CloudKit framework is organized by looking at the most important classes for managing data in the cloud. There are several key concepts: Containers, Records, Relationships, and Assets. We will introduce each of them in turn, starting with Containers.

Containers —How Data Access Is Organized

The data management and data access in iCloud have a similar feel to how we access data locally on the device in that each application has its own sandbox in the cloud. This sandbox is called a Containerand contains the data that an app might need to store in the cloud. One significant difference, however, is that, unlike with local storage, several apps can share a storage container in the cloud, as long as they are all associated with the same development account.

Programmatically containers are represented by the CKContainer class, which is part of the CloudKit framework. Every app that uses iCloud has a designated default container object, which you can obtain by calling CKContainer.defaultContainer()(Listing 13-12).

Listing 13-12. Obtaining a Reference to the Default Container
let container = CKContainer.defaultContainer()

Figure 13-2 illustrates the structure of a container. It consists of several databases: a public one and multiple private ones. The public database is application-centric and is available to all users of the app, whereas each private database is associated with only one user and is only accessible to that user.

A371202_1_En_13_Fig2_HTML.jpg
Figure 13-2. CloudKit Container structure

When an app is installed on a user’s device, it can only see and access two databases: the public one and the private database for the user. CKContainer has helper methods, which allow us to talk to each database: publicCloudDatabase and privateCloudDatabase. Each of these methods returns a reference to a CKDatabase object (Listing 13-13).

Listing 13-13. Obtaining References to the Public and the Private Database
let publicDatabase = container.publicCloudDatabase
let privateDatabase = container.privateCloudDatabase
Note

There is a certain amount of data storage and data transfer available for free from Apple; going over that requires a paid subscription plan. As a rule of thumb, data stored in the public database counts toward the application’s quota and data stored in the private database counts toward the user’s quota. It’s a good idea to try to minimize the amount of data you store in the user’s private database to avoid forcing users to purchase additional storage space.

Be aware that you cannot delete a container. Once a container is created it will stay in iCloud forever, or at least until Apple decides to change how its cloud works. In other words, do not run around and create new containers without a good reason.

Next we will have a look at how data is represented inside a container’s database.

Records —How Data Is Represented

Data in the CloudKit databases is organized as records. A record is very similar to a key-value pair, where the value can take a variety of types: number, array, date, and time; location; binary blob; and many more. The key part of the pair is of type String, which consists of alphanumeric characters and starts with a letter.

The class that helps you work with records is called CKRecord. CKRecord instances have a property, called recordType, which serves as metadata: it holds a string with a custom description of the record’s data. This gives us a way of querying iCloud for records that match this description. This is helpful if we want to query iCloud for certain record types only. You can give any valid string name as a record type. When a record is created in the cloud it automatically gets some additional metadata fields that store information about when the record was created, which user created it, when it was last updated, and who made the update. You can use this metadata to narrow your searches in the database.

Let us see how we can create a record and fill it in with data Listing 13-14). An instance of the CKRecord class is created by calling its default initializer and passing a string to describe the record type. To add a key-value pair we call the setObject(_:forKey:) method of the instance. The listing also shows you a shortcut alternative: instead of calling setObject(_:forKey:) you can put the key name in square brackets, [], and use the assignment operator.

Note

There are two important rules to follow when naming your keys: you can’t use spaces and a key name must not match the name of any of the properties of the CKRecord class.

Listing 13-14. Creating a CKRecord Instance and Populating It with Data
let record = CKRecord(recordType: "Person")
record.setObject("Julius Marx", forKey: "name")
record["age"] = 30
record["nickname"] = "Groucho"

Each record has a unique ID, which is automatically assigned to it by the CloudKit framework at the time of initialization. You can set an ID yourself by using the initWithRecordType:recordID: initializer of the CKRecord class. When a record is received by the iCloud server its ID gets checked for uniqueness and if it is not unique the server throws an error.

Records can be organized in groups, called zones, which are represented by the CKRecordZone class. Every container has a default zone, which is used if you don’t explicitly define one. Grouping records lets you define custom business logic and perform transactional operations on records in a zone. We won’t use zones in this tutorial, but it’s good to be aware of them, as they can be invaluable when it comes to segmenting big data.

Defining Relationships Between Records

Imagine we have to create a music player application that will help users play their favorite songs of their favorite bands. We can design the underlying data model as records, which will represent a band, an album, or a song. In other words, each CKRecord in our model will have its recordType set to Bands, Albums, or Songs. If we had the record for a song, it would be useful to be able to work out what album it was released on and which band recorded it. To do that we will need to set relationships between the song, album, and band records.

In CloudKit relationships between records are managed by instances of the CKReference class. A CKReference object creates a many-to-one relationship between records in CloudKit’s database. Records that refer to another record, which we will call the target record, need to contain a reference object, which in turn contains information about the target. Both the target and the referring records need to be in the same zone of the same database.

Listing 13-15 illustrates how references between records are created, using the example from the start of this section: we see how to create references from a song record to its respective band and album records.

Listing 13-15. Creating a Reference Between Two CKRecords
let band = CKRecord(recordType: "Bands")
band["name"] = "Scorpions"


let album = CKRecord(recordType: "Albums")
album["name"] = "Crazy World"


let song = CKRecord(recordType: "Songs")
song["name"] = "Wind of Change"


let referenceBand = CKReference(recordID: band.recordID,
    action: .DeleteSelf)
let referenceAlbum = CKReference(recordID: album.recordID,
    action: .DeleteSelf)


song["inAlbum"] = referenceAlbum
song["byBand"] = referenceBand

You probably noticed a parameter called action, which we set when initializing CKReference. This is a property of the CKReference class, which defines what action needs to be taken when the target of the reference is deleted. We can choose between these two:

  • CKReferenceActionDeleteSelf—deleting the target record deletes any records that contain that reference in one of their fields.

  • CKReferenceActionNone—no action is taken.

Assets—Storing Unstructured Data

Storing strings and numbers in the cloud is good, but how do we go about storing a large document like an image or a video file? To create such a record in the database, its value needs to be of type CKAsset. This class represents data stored in, typically, large files.

Listing 13-16 builds on the band-album example we used earlier and stores the album cover as an asset.

Listing 13-16. Creating an Asset from an Image File
let albumCoverAsset = CKAsset(fileURL: coverImageURL)
album["coverImageAsset"] = albumCoverAsset
Note

Even though we initialized the CKAsset class with a URL (uniform resource file) to a file, the URL itself is not stored in the cloud. Only the asset’s data gets uploaded to the cloud.

Saving and Deleting Records from the Cloud

CKRecord instances live in the application’s memory and get deleted when the app closes. To persist a record to iCloud, we need to ask the database to which it belongs to save it on the server. For that purpose CKDatabase has a convenience method, called saveRecord(_:completionHandler:). Have a look at Listing 13-17 for how to use it. saveRecord’s first argument is the record we want to persist and its second argument is a callback, also known as completion handler, which will be executed when the saving operation finishes. The callback’s signature is (CKRecord?, NSError?): in it we get passed the record we tried to persist and an error object, which is set to nil if the saving was successful or contains information on what went wrong otherwise.

Listing 13-17. Save a CKRecord in the Server’s Database
let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase
let band = CKRecord(recordType: "Bands")
band["name"] = "Scorpions"


privateDatabase.saveRecord(band) { (record, error) -> Void in
    //Handle success or error response from the server
}

saveRecord(_:completionHandler:) is used for updating existing records in the database, as well as for persisting them for the first time.

To delete a record from the server we call CKDatabase’s deleteRecordWithID(_:completionHandler:) method, which looks very similar to the method we used for saving: we pass in the record we want to delete and a callback function as arguments. The callback gets executed when the deletion is complete; it receives a reference to the record that we tried to delete and optional error information. We can see this in Listing 13-18.

Listing 13-18. Deleting a Record from the Cloud
privateDatabase.deleteRecordWithID(band.recordID)
    {(recordID, error) -> Void in }

Note that saving and deleting are performed asynchronously over the network. As it is not in our control how long a response from the server may take, it is usually a good idea to notify the user with an activity indicator that a potentially lengthy operation is taking place.

Retrieving Records from the Cloud

To get records from the cloud we need to perform a query, using CKQuery—another helper class from the CloudKit framework. Initializing a CKQuery instance takes the already familiar recordType and an instance of NSPredicate. NSPredicate is a powerful class that helps us specify how data should be retrieved from the database. It has its own query language, similar to using the WHERE clause in the structured query language (SQL) for those of you familiar with SQL.

Apart from narrowing down and filtering the data we fetch from the database, we can have the retrieved records sorted, using CKQuery’s property sortDescriptors, which is an array of NSSortDescriptor objects.

Performing a query on the server is an asynchronous operation and, like the rest of the server-side calls we have made so far, requires that we implement a completion handler function that will be called once the query returns a result. We pass this completion handler to CKDatabase’s performQuery(_:inZoneWithID:completionHandler:) method.

In Listing 13-19 you can see an example of a query that retrieves all records of type Bands from the server and sorts them by their name in ascending order. The result comes as an array of CKRecord objects, which is the first parameter of the completion handler closure we pass to privateDatabase.performQuery.

Listing 13-19. Retrieve a List of Records from the Server
let fetchQuery  = CKQuery(recordType: "Bands",
                         predicate: NSPredicate(format: "TRUEPREDICATE"))


fetchQuery.sortDescriptors = [ NSSortDescriptor(key: "name",
                                ascending: true) ]


privateDatabase.performQuery(fetchQuery, inZoneWithID: nil) {
                                (records, error) -> Void in }

Common Error Messages

As you can see, all network operations we have done so far return their results in callback functions, which take optional NSError parameters. It’s a good idea always to check if the error passed to the callback is nil and if it isn’t, to inspect its error code. You can find a complete list of error codes in the definition of CKErrorCode. Following are some of the most common ones with suggestions about how to deal with them:

  • .BadContainer or .MissingEntitlement. Check if you have specified a container in the iCloud entitlements section of your app matching the CKContainer object. Make sure that the container exists on the CloudKit Dashboard web site.

  • .NotAuthenticated or .PermissionFailure. You might not have entered your iCloud credentials in the settings of your device yet. Go to device Settings > iCloud and see if an account has been assigned. Another reason to get one of these errors could be that iCloud is disabled on the device. Enable it in Settings ➤ iCloud.

  • .UnknownItem. This usually means that you have requested a record type that doesn’t exist on the server. Make sure you haven’t made a typo when specifying the string for recordType and check the CloudKit Dashboard web site to make sure the string matches the records that are there.

I keep mentioning the CloudKit Dashboard but haven’t told you what it is yet. This is a web-based admin panel that helps you manage data on the iCloud server. The rest of this chapter provides a more in-depth look at it.

Building a CloudKit-Powered Application

At this point you should be familiar with the basic concepts in the CloudKit framework. Now let us put all of them into practice by building an app.

The app will maintain a simple shopping list, which the user will create by adding or deleting items from it. The app will persist this shopping list to the user’s iCloud private account and will take care of synchronizing the user’s local copy with the one in the cloud. When the app is run for the first time, it will be the user who creates the shopping list. After that, every time we start the application, it will retrieve the list stored on the server and show it to the user for modification.

I have divided the app implementation into three parts:

  • Setup: we will set up the prerequisites necessary to enable an app to work with iCloud;

  • Data management: this will require us to write some code for communicating with the iCloud;

  • User interface: we will see how we can present data to the user.

Note

To be able to test the code in this tutorial you will need an Apple ID that’s a member of the iOS Development Program.

Preparation: Setting Up the App

First things first. Before we start coding, we need to perform several mandatory steps to enable CloudKit in our application.

Start by creating a new Single View Application project in Xcode and name it CloudKitDemo. Select the project in the Project Navigator and select the app from the list of targets. Open the General tab and set Team to the correct iOS development account (Figure 13-3).

A371202_1_En_13_Fig3_HTML.jpg
Figure 13-3. Select an account in Team: it will need to be a member of Apple’s iOS Development Program

Next, open the Capabilities tab and find the iCloud setting. Toggle the switch to enable iCloud (Figure 13-4). This will add all the necessary entitlements to your application. It may take a few minutes for Xcode to create an app ID and authorize it to use iCloud. Note that this step will generate an error if the team/Apple ID you selected in the previous step is not a member of Apple’s iOS Development Program.

A371202_1_En_13_Fig4_HTML.jpg
Figure 13-4. Enable iCloud usage for the app target

Tick the Key-value storage and CloudKit checkboxes (Figure 13-5). In this tutorial we will not share database containers with other apps, so leave the Use default container option checked.

A371202_1_En_13_Fig5_HTML.jpg
Figure 13-5. The applciation's iCloud settings

At the bottom of the settings you will see a button that says CloudKit Dashboard. Click it to go to the CloudKit Dashboard admin web page—it will load in a browser. Log in with your Apple ID.

Note

If your Apple ID is a member of multiple teams, you will need to first make sure that you are already logged into developer.apple.com and have chosen the right team. Otherwise you will see an error indicating that nothing has been set up yet.

CloudKit Dashboard —Registering Record Type

The CloudKit Dashboard is the place where you can administer the app’s back end in the cloud. It lets you manage database containers, zones, and record types; monitor data usage and data traffic; and get information about users.

Before we can store and fetch records from iCloud we need to create a new Record Type. Open the CloudKit Dashboard page and select Record Types from the menu on the left as shown on Figure 13-6. Create a new record type by clicking the + button at the top of the page. Name the new record type ShoppingItem. Next you need to add fields to the record and specify their data type. Let us add a field of type String and name it “name.” Finally, click the Save button at the bottom of the page to persist the changes.

A371202_1_En_13_Fig6_HTML.jpg
Figure 13-6. Create a new Record Type

Linking the Device with an iCloud Account

For our users to be able to use this application they should be logged in with their iCloud account on the device. You have to do the same in order to test the app whether on a device or in a simulator.

On your iOS device open Settings ➤ iCloud and make sure iCloud Drive is turned on (Figure 13-7). Then, if the account field is empty, fill in your or your Apple ID credentials to log in to iCloud.

A371202_1_En_13_Fig7_HTML.jpg
Figure 13-7. Log in to iCloud on a device or in a simulator

Using the CloudKit Framework: Making a Cloud Data Manager

With the project and the new record type set up, it is time to start coding with the CloudKit framework. For clarity we will split the application’s code into two layers. One layer will manage shopping list items and will communicate with the iCloud back end and other layer will take care of the user interface.

In this section we will create a class which will be responsible for

  • storing all shopping list items and providing access to their names.

  • adding an item to the list and sending it to the cloud.

  • deleting an item from the list and syncing with the cloud.

  • notifying the UI of the application whenever a response from the server arrives.

From Xcode’s main menu select File New File and click Swift fil e to add a new source code file to the project. Name the file CloudDataManager.swift and click on its name in Project navigator to open it. Listing 13-20 shows the CloudDataManager class definition with all the methods stubbed out. We will add functionality to them in a minute. Note that in order to use CloudKit’s API, we need to import the CloudKit framework at the top of the file.

Listing 13-20. CloudDataManager Class
import Foundation
import CloudKit


class CloudDataManager {
    //Shopping list items array
    var dataItems : [CKRecord]


    //Record Type of the items
    let recordType = "ShoppingItem"


    //Default initializer
    init() {
        //Create an empty array of shopping items
        dataItems = []
    }


    //Get item's name at a given index in the items array
    func getItemName( atIndex index : Int ) -> String {
        return dataItems[index]["name"] as! String
    }


    //Get the number of shopping items
    func getItemsCount() -> Int {
        return dataItems.count
    }


    //Get all ShoppingItem records from the server and load them into the list
    func fetchCloudItems( callback callback: (NSError?) -> Void ) {
        //Code of the method goes here
    }


    //Add a new shopping item to the list and sync with iCloud
    func addItem( itemName item : String, callback: (NSError?) -> Void ) {
        //Code of the method goes here
    }


    //Remove an existing shopping item from the list and sync with iCloud
    func removeItem( atIndex index : Int, callback: (NSError?) -> Void) {
        //Code of the method goes here
    }
}

In this class dataItems property is an array that stores the local cache for the shopping list items. There are two sets of methods in the class. The first set includes getItemName(:atIndex) and getItemsCount(), which act as a façade for dataItems. They are used to query the local cache and do not perform any network operations, so they are fast and synchronous.

The next three methods, fetchCloudItems(:callback), addItem(:itemName:callback), and removeItem(:atIndex:callback) perform asynchronous network operations and are potentially slow. Because of this, each of the methods takes a callback function as an argument, which is executed when the respective network operation is complete. We will later implement these callbacks to update the app’s user interface.

We have the skeleton of the CloudDataManager class. Let us now focus on implementing its logic in each of the methods’ stubs (Listing 13-21).

We will start with fetchCloudItems(:callback). This method is called when the app launches: its purpose is to get all previously entered shopping items from the server if there are any. For that purpose we will create a query for retrieving all records of type ShoppingItem: this will be done with the help of an NSPredicate instance. In Listing 13-21 we set the NSPredicate’s filter to TRUEPREDICATE, which means “iterate through all records and fetch each one.” The next step is to get the records sorted. Sorting will be performed on the records’ “name” field in ascending order. The result of the query is returned in the records argument of the completion handler, implemented as a closure in this case.

Note

For details on how closures work in Swift, see Chapter 22.

In the completion handler we are going to update the local cache and signal the UI to show the updated shopping list. The logic of the code inside the closure is fairly simple: if the result contains any records, they are copied into the local list of items. Finally, the code calls the callback argument to communicate the changes with the UI. This is a pattern we will follow when implementing the rest of the methods that involve waiting for a response from the server.

Listing 13-21. Implementation of fetchCloudItems Method
func fetchCloudItems( callback callback: (NSError?) -> Void ) {
    //Get a reference of the user's iCloud database
    let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase


    //Construct a query to get all the records of type "ShoppingItem"
    let fetchQuery  = CKQuery(recordType: self.recordType,
                    predicate: NSPredicate(format: "TRUEPREDICATE"))


    //Sort fetched records by their name in ascending order
    fetchQuery.sortDescriptors = [ NSSortDescriptor(key: "name",
                                    ascending: true) ]


    //Send the query to iCloud
    privateDatabase.performQuery(fetchQuery, inZoneWithID: nil) {
        (records, error) -> Void in
        //Move the result records into the dataItems array
        if records!.count > 0 {
            for record in records! {
                self.dataItems.append( record )
            }
        }


        //Tell the UI to update itself
        callback(error)
    }
}

Next to implement is the addItem(:itemName:callback) method (Listing 13-22). This method is called every time the user adds an item to the shopping list and a new CKRecord instance needs to be created for it. Once this is done the new record is persisted to the cloud by the saveRecord call. We only add the new record to the local cache if saving it to the server was successful—we are notified about this in the completion handler we supply to saveRecord.

Listing 13-22. Implementation of addItem Method
func addItem( itemName item : String, callback: (NSError?) -> Void ) {
    //Get a reference of the user's iCloud database
    let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase


    //Create a record of of type "ShoppingItem" and set its name
    let localRecord = CKRecord(recordType: self.recordType)
    localRecord["name"] = item


    //Ask iCloud to store the record
    privateDatabase.saveRecord(localRecord) { (record, error) -> Void in
        //If the response is ok add this record to the local list
        if let record = record {
            self.dataItems.append( record )
        }


        //Tell the UI to update itself
        callback( error )
    }
}

We will complete the CloudDataManager class with the implementation of the removeItem(:atIndex:callback) method (Listing 13-23). We ask the server to delete a record by calling deleteRecordWithID and passing the record’s unique ID, which we can work out from the record’s recordID property. The completion handler we supply follows a familiar pattern: if the deletion is successful, the local cache is updated and changes are reflected in the user interface.

Listing 13-23. Implementation of removeItemAtIndex Method
func removeItem( atIndex index : Int, callback: (NSError?) -> Void) {
    //Get a reference of the user's iCloud database
    let privateDatabase = CKContainer.defaultContainer().privateCloudDatabase


    //Get a record by its index in the local list
    let record = dataItems[ index ]


    privateDatabase.deleteRecordWithID(record.recordID) {
     (recordID, error) -> Void in
        //Get the local index of the record that should be removed
        let recordIndex = self.dataItems.indexOf(record)
        //Remove the underlying record
        self.dataItems.removeAtIndex(recordIndex!)


        //Tell the UI to update itself
        callback( error )
    }
}

Preparing the User Interface

We will use a Table View component to display the shopping list on the screen and two buttons that will let the user add and delete items.

In Xcode open the Main.storyboard file and make it look like the screenshot in Figure 13-8: first find a Navigation bar component in the Object library and drag it to the top of the scene. Then drag two Bar Button item components on top of it—place one on the left and one on the right. Select the left button and in the Attributes inspector select Edit from the System Item drop-down. This will change the caption of the button to Edit. Set System Item for the right button to Add and its caption will change into a big plus sign. These two buttons will let the user add and delete items from the list.

A371202_1_En_13_Fig8_HTML.jpg
Figure 13-8. CloudKitDemo application user interface

To finish building the UI drag and drop a Table view component under the Navigation and add constraints to it to take the remaining portion of the screen. Create a prototype cell and set its Identifier property to ShoppingItemCell.

Note

For more info about table views, prototype cells, cell reusing, and identifiers, see Chapter 6.

Drag the table view into the definition of the ViewController class in ViewController.swift to create an outlet. Name the outlet tableView (Listing 13-24).

Listing 13-24. Create an Outlet for the Table View
@IBOutlet weak var tableView: UITableView!

Creating Table View Data Source and Delegate

The table view needs to get its data from an object that conforms to the UITableViewDataSource protocol. To keep things simple, we will make the ViewController class our data source. To talk to the table view, we will also make it conform to the UITableViewDelegateprotocol (Listing 13-25).

Listing 13-25. Conform to UITableViewDelegate and UITableViewDataSource Protocols
class ViewController: UIViewController, UITableViewDelegate,
    UITableViewDataSource {


    //The rest of the code goes here
}

Conforming to these two protocols requires implementation of these two methods in the ViewController class: tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAtIndexPath). We will just stub them out as shown in Listing 13-26.

Listing 13-26. Implement UITableViewDelegate and UITableViewDataSource Protocols' Methods
class ViewController: UIViewController, UITableViewDelegate,
    UITableViewDataSource {


    //The rest of the code goes here

    func tableView(tableView: UITableView,
        numberOfRowsInSection section: Int) -> Int {
        return 0
    }


    func tableView(tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

Populating the Table View with Data

We are finally ready to fill the table view with data. First we need an instance of the CloudDataManager class, which will provide shopping items data: we will make this a property of the ViewController class, called cloudDataManager. The next step is to set the table view’s data source and delegate properties to the ViewController class instance. We will do this in the viewDidLoad() method of ViewController (Listing 13-27).

Listing 13-27. Instantiate CloudDataManager and Fetch Shopping Items from the Server
class ViewController: UIViewController, UITableViewDelegate,
    UITableViewDataSource {


    //Reference to the table view
    @IBOutlet weak var tableView: UITableView!


    //A reference to the data manager class
    let cloudDataManager = CloudDataManager()


    override func viewDidLoad() {
        super.viewDidLoad()


        //Set table view datasource and delegate
        tableView.delegate = self
        tableView.dataSource = self


        //Get all shopping list items from the server
        cloudDataManager.fetchCloudItems( callback: dataChanged )
    }


    //The rest of the code goes here
}

There is a method called dataChanged, which we haven’t implemented yet. This method will be used as a callback for the CloudDataManager methods and will notify the UI to update itself when there is a response from the server.

Its implementation is pretty straightforward : if there was an error, we will print out information about in Xcode’s console; otherwise we will update the table view (Listing 13-28). In iOS we are allowed to access the UI only on the main thread and we need do make sure the all related calls happen there. To execute a code block on the main thread we wrap it in a dispatch_async(dispatch_get_main_queue(),{() -> Void}) call. This function tells iOS to execute the code block nested inside on a specific thread of the app. dispatch_get_main_queue makes sure that the chosen thread is the main one.

Listing 13-28. Update the Table View Using a Callback Method
func dataChanged( error : NSError? ) {
    if let error = error {
        //If there is an error message print it in the console
        print(error)
    }
    else {
        //When there is a response do the logic in the main thread
        dispatch_async(dispatch_get_main_queue(), { å
         [weak self] () -> Void in
            //Refresh data in the table view
            self!.tableView.reloadData();
        })
    }
}
Note

There are a couple of interesting things to note in the code in Listing 13-28. First, we make a call to dispatch_async, which causes code to be executed on a particular queue. The code we pass will update the UI, so we select the main queue. We covered queues in Chapter 7—have another look if you need a reminder. The block of code that we want executed on the main queue is passed in as the second parameter to dispatch_async in the form of a closure—you can see it wrapped in curly brackets. Chapter 22 presents closures in detail. The last thing that may look strange at first is the reference to [weak self], where self refers to the current instance of ViewController. As controversial as this looks, it is not a judgment on our class’s character but has to do with memory management. As closures are independent objects, they retain strong references; that is, they obtain ownership of the objects they reference. To prevent that, we mark the reference as weak. You can read more on weak references and memory management in Chapter 21

Now let us populate the shopping items in the table view. First we need to provide the number of items so the table can ask the data source to construct the appropriate number of rows in the table. The number of rows is set in the tableView(_:numberOfRowsInSection:) method (Listing 13-29).

Listing 13-29. Set Table View Rows Number
func tableView(tableView: UITableView,
    numberOfRowsInSection section: Int) -> Int
{
    //Return shopping items number
    return cloudDataManager.getItemsCount()
}

The tableView(_:cellForRowAtIndexPath:) call gives us access to update a single cell in the table: we will fill in the new cells with the names of the items in the shopping list, as shown in Listing 13-30.

Listing 13-30. Create a Cell and Set Its Label
func tableView(tableView: UITableView,
    cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    //Create a new table cell for the row
    let cell = tableView.dequeueReusableCellWithIdentifier(
                            "ShoppingItemCell", forIndexPath: indexPath)


    //Set the cell's label to be the name of the corresponding shopping item
    cell.textLabel?.text = cloudDataManager.getItemName(
                                                atIndex: indexPath.row)


    return cell
}

Run the app and you will see an empty table (Figure 13-9). In the next section we will implement how items are added, so we can create our first shopping list.

A371202_1_En_13_Fig9_HTML.jpg
Figure 13-9. Empty table view

Adding New Items

Open Main.storyboard and ViewController.swift in the Assistant editor and create an Action handler for the plus button. Name this Action didTapAdd (Listing 13-31).

Listing 13-31. Add an Action Handler for Adding New Items
@IBAction func didTapAdd(sender: UIBarButtonItem) {
}

We will show an alert dialog to the user to type the new item’s name. Listing 13-32 shows you how to create an alert dialog with a text field and two buttons: Cancel and Add.

Listing 13-32. Add Item Dialog Implementation
@IBAction func didTapAdd(sender: UIBarButtonItem) {
    //Create an allert controller dialog for adding new item
    let alertController = UIAlertController(title: "New Item",
        message: "Tap in new item", preferredStyle: UIAlertControllerStyle.Alert)


    //Add a text field component to the dialog
    alertController.addTextFieldWithConfigurationHandler {
        (UITextField) -> Void in }


    //Add a Cancel button to dismiss the dialog
    alertController.addAction(UIAlertAction(title: "Cancel",
    style: UIAlertActionStyle.Default, handler: {
        (action : UIAlertAction) -> Void in
    }))


    //Add an Add button to add new item
    alertController.addAction(UIAlertAction(title: "Add",
    style: UIAlertActionStyle.Default, handler: {
        (action : UIAlertAction) -> Void in


        //Get the item's name from dialog's text field
        let itemName = (alertController.textFields![0] as UITextField).text


        //Ask data manager to create new shopping item with the given name
        self.cloudDataManager.addItem( itemName: itemName!,
            callback: self.dataChanged)
    }))


    //Show this dialog on the screen
    self.presentViewController(alertController, animated: true,
        completion: nil)
}

Run the application and tap the + button. You should see the dialog prompting you to create a new item (Figure 13-10).

A371202_1_En_13_Fig10_HTML.jpg
Figure 13-10. Add new item dialog

Deleting Items from the Table View

While adding a new item required us to write only one new method, deleting an item will require more work. We will add several new methods, but they will all be one-liners, I promise.

First create an Action handler for the Edit button in ViewController.swift. Name the Action didTapEdit. Inside this method we will toggle the tableView.editing property to true or false (Listing 13-33). Changing this property notifies the table that we want to edit its rows and causes each row to show a delete button.

Listing 13-33. Edit Button Action
@IBAction func didTapEdit(sender: UIBarButtonItem) {
    //Toggle table view edit mode
    tableView.editing = !tableView.editing
}

When the table view is in edit mode it will ask the data source to verify if a given row can be edited. This would require implementing a method called UITableViewDataSource tableView(_:canEditRowAtIndexPath). Since all of our shopping items can be deleted we will always return true (Listing 13-34).

Listing 13-34. A Method That Checks If a Given Row in the Table Can Be Edited
func tableView(tableView: UITableView,
    canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {


    //We can edit all the rows
    return true
}

When the user taps the delete button next to a row, the table view asks the data source to commit the deletion of the row. This is done inside the tableView(_: commitEditingStyle: forRowAtIndexPath) method (Listing 13-35). This method is used for committing any editing operations, so before proceeding further we will need to make sure that we are committing a deletion: the value of the editingStyle argument needs to be .Delete. Finally, we ask cloudDataManager to delete the corresponding shopping item.

Listing 13-35. A Method for Deleting a Row
func tableView(tableView: UITableView,
    commitEditingStyle editingStyle: UITableViewCellEditingStyle,
        forRowAtIndexPath indexPath: NSIndexPath) {


    if UITableViewCellEditingStyle.Delete == editingStyle {
        //Ask the data manager to delete the coresponding shopping item
        cloudDataManager.removeItem( atIndex: indexPath.row,
            callback: dataChanged)
    }
}

The app is now complete. Run it, tap Edit, and delete a row to see it vanish from the table view and from the iCloud servers (Figure 13-11).

A371202_1_En_13_Fig11_HTML.jpg
Figure 13-11. Deleting an item from the list

Summary

This chapter gave you the tools for working with files on the local device as well as storing and retrieving data from Apple’s iCloud service. You saw a few different ways to persist data: as a binary blob, via serialization, and even via using the application’s settings database.

With this knowledge we are ready to head off to the land of networking operations. See you there.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset