2011年10月31日星期一

Beginning iCloud in iOS 5 Tutorial Part 1

Beginning iCloud in iOS 5 Tutorial Part 1:
Learn how to use iCloud in iOS 5!
Learn how to use iCloud in iOS 5!

Note from Ray: This is the ninth iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Enjoy!

This is a post by iOS Tutorial Team member Cesare Rocchi, a UX designer and developer specializing in web and mobile applications.

We all have “stuff” we use on our iPhones and iPads regularly like documents, pictures, videos, emails, calendars, music, and address books. But how many times have you tried to quickly open a document and realized “argh, I have it saved onto another device”?

Well, with the new iCloud feature in iOS 5, that is problem of the past!

iCloud is a service that helps you synchronize your data across devices. It is a set of central servers which store your documents, and make the latest version available to every device/app compatible with iCloud (iPhone, iPod, iPad, Mac, or even PC).

In this tutorial, we’ll investigate iCloud by implementing a set of simple applications which interact with cloud servers to read, write and edit documents. In the process, you’ll learn about the new UIDocument class, querying iCloud for files, autosaving, and much more!

Note to get the most out of this tutorial, you will need two physical iOS devices running iOS 5 for testing, such as an iPhone and an iPad. The simulator does not currently have iCloud support.



Under the Hood


Before we begin, let’s talk about how iCloud works.

In iOS each application has its data stored in a local directory, and each app can only access data in its own directory. This prevents apps from reading or modifying data from other apps (although there are some alternate methods of transferring data between apps built into the OS).

iCloud allows you to upload your local data to central servers on the net, and receive updates from other devices. The replication of content across different devices is achieved by means of a continuous background process (daemon) which detects changes to a resource (document) and uploads them to the central storage.

This works real-time and enables another interesting feature: notifications. For example, whenever there is a conflict about a document, the application can be aware of that and you can implement a resolution policy.

If you ever tried to create something like this with your own apps, you know there are several major challenges implementing this:

  • Conflict resolution. What happens if you modify a document on your iPhone, and modify the same document on your iPad at the same time? You somehow have to reconcile these changes. iCloud allows you to break your documents into chunks to prevent many merge conflicts from being a problem (because if you change chunk A on device 1, and chunk B on device 2, since chunk A and B are different you can just combine them). For cases when it truly is a problem, it allows you as a developer fine-grained control over how to handle the problem (and you can always ask the user what they would like to do).
  • Background management. iOS apps only have limited access to running tasks in the background, but keeping your documents up-to-date is something you want to always be doing. The good news is since iCloud synchronization is running in a background daemon, it’s always active!
  • Network bandwidth costs. Continuously pushing documents between devices can take a lot of network bandwidth. As mentioned above, iCloud helps reduce the costs by breaking each document into chunks. When you first create a document, every chunk is copied to the cloud. When subsequent changes are detected only the chunks affected are uploaded to the cloud, to minimize the usage of bandwidth and processing. A further optimization is based on a peer-to-peer solution. That happens when two devices are connected to the same iCloud account and the same wireless network. In this case data take a shortcut and move directly between devices.

The mechanisms described so far are enabled by a smart management of metadata like file name, size, modification date, version etc. This metadata is pushed to the cloud, and iCloud uses this information to determine what needs to be pulled down to each device.

Note that devices pull data from the cloud when “appropriate”. The meaning of this depends on the OS and platform. For example an iPhone has much less power and “battery dependency” than an iMac plugged into a wall. In this case iOS might decide to notify just the presence of a new file, without downloading it, whereas Mac OS X might start the download immediately after the notification.

The important aspect is that an app is always aware of the existence of a new file, or changes to an already existing file, and through an API the developer is free to implement the synchronization policy. In essence the API allows an app to know the “situation” on iCloud even if the files are not yet local, leaving the developer free to choose whether (and when) to download an updated version.

Configuring iCloud in iOS 5


The first time you install iOS 5, you’ll be asked to configure an iCloud account by providing or creating an Apple ID. Configuration steps will also allow you to set which services you want to synchronize (calendar, contacts, etc.). Those configurations are also available under Settings\iCloud on your device.

Before you proceed any further with this tutorial, make sure that you have iOS 5 installed on two test devices, and that iCloud is working properly on both devices.

One easy way to test this is to add a test entry into your calendar, and verify that it synchronizes properly between your various devices. You can also use http://www.icloud.com to see what’s in your calendar.

Once you’re sure iCloud is working OK on your device, let’s try it out in an app of our own creation!

Enabling iCloud in your App


In this tutorial, we’ll be creating a simple app that manages a shared iCloud document called “dox”. The app will be universal and will be able to run on both iPhone and iPad, so we can see changes made on one device propagated to the other.

There are three steps to use iCloud in an app, so let’s try them out as we start this new project.

1. Create an iCloud-enabled App ID

To do this, visit the iOS Developer Center and log onto the iOS Provisioning Portal. Create a new App ID for your app similar to the below screenshot (but replace the bundle identifier with a unique name of your own).

Note: Be sure to end your App ID with “dox” in this tutorial, because that is what we will be naming the project. For example, you could enter com.yourname.dox.

Creating an App ID

After you create the App ID, you will see that Push Notifications and Game Center are automatically enabled, but iCloud requires you to manually enable it. Click the Configure button to continue.

Configuring an App ID to use iCloud

On the next screen, click the checkbox next to Enable for iCloud and click OK when the popup appears. If all works well, you will see a green icon next to the word Enabled. Then just click Done to finish.

Clicking Enable for iCloud in the iOS Provisioning Portal

2) Create a provisioning profile for that App ID

Still in the iOS Provisioning Portal, switch to the Provisioning tab, and click New Profile. Select the App ID you just created from the dropdown, and fill out the rest of the information, similar to the below screenshot:

Creating a Provisioning Profile

After creating the profile, refresh the page until it is ready for download, and then download it to your machine. Once it’s downloaded, double click it to bring it into Xcode, and verify that it is visible in Xcode’s Organizer:

Viewing Provisioning Profiles in Xcode Organizer

3) Configure your Xcode project for iCloud

Start up Xcode and create a new project with the iOS\Application\Single View Application template. Enter dox for the product name, enter the company identifier you used when creating your App ID, set the device family to Universal, and make sure Use Automatic Reference Counting is checked (but leave the other checkboxes unchecked):

Creating a new app in Xcode

Once you’ve finished creating the project, select your project in the Project Navigator and select the dox target. Select the Summary tab, and scroll way down to the Entitlements section.

Once you’re there, click the Enable Entitlements checkbox, and it will auto-populate the other fields based on your App ID, as shown below:

Setting entitlements in Xcode

This is what the fields here mean:

  • The Entitlements File points to a property list file which, much like the info.plist file, includes specifications about application entitlements.
  • The iCloud Key-Value Store represents the unique identifier which points to the key-value store in iCloud.
  • The iCloud Containers section represents “directories” in the cloud in which your application can read/write documents. Yes, you have read correctly, I said applications (plural), for a user’s container can be managed by more than one application. The only requirement is that applications have to be created by the same team (as set up in the iTunes Developer Center).
  • The Keychain Access Groups includes keys needed by applications which are sharing keychain data.

You don’t have to change anything from the defaults for this tutorial, so you’re ready to go! If you like you can edit the same settings by editing the file dox.entititlements which is included in your project.

Checking for iCloud Availability


When building an application which makes use of iCloud, the best thing to do is to check the availability of iCloud as soon as the application starts. Although iCloud is available on all iOS 5 devices, the user might not have configured it.

To avoid possible unattended behaviors or crashes, you should check if iCloud is available before using it. Let’s see how this works.

Open up AppDelegate.m, and add the following code at the bottom of application:didFinishLaunchingWithOptions (before the return YES):


NSURL *ubiq = [[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil];
if (ubiq) {
NSLog(@"iCloud access at %@", ubiq);
// TODO: Load document... 
} else {
NSLog(@"No iCloud access");
}


Here we use a new method you haven’t seen yet called URLForUbiquityContainerIdentifier. This method allows you to pass in a container identifier (like you set up earlier in the iCloud Containers section) and it will return to you a URL to use to access files in iCloud storage.

You need to call this on startup for each container you want to access to give your app permission to access the URL. If you pass in nil to the method (like we do here), it automatically returns the first iCloud Container set up for the project. Since we only have one container, this makes it nice and easy.

Compile and run your project (on a device, because iCloud does not work on the simulator), and if all works well, you should see a message in your console like this:

iCloud access at file://localhost/private/var/mobile/Library/Mobile%20
Documents/KFCNEC27GU~com~razeware~dox/

Note that the URL this returns is actually a local URL on the system! This is because the iCloud daemon transfers files from the central servers to a local directory on the device on your behalf. Your application can then retrieve files from this directory, or send updated versions to this directory, and the iCloud daemon will synchronize everything for you.

This directory is outside of your app’s directory, but as mentioned above the act of calling URLForUbiquityContainerIdentifier gives your app permission to access this directory.

iCloud API Overview


Before we proceed further with the code, let’s take a few minutes to give an overview of the APIs we’ll be using to work with iCloud documents.

To store documents in iCloud, you can do things manually if you’d like, by moving files to/from the iCloud directory with new methods in NSFileManager and the new NSFilePresenter and NSFileCoordinator classes.

However doing this is fairly complex and unnecessary in most cases, because iOS 5 has introduced a new class to make working with iCloud documents much easier: UIDocument.

UIDocument acts as middleware between the file itself and the actual data (which in our case will be the text of a note). In your apps, you’d usually create a subclass of UIDocument and override a few methods on it that we’ll discuss below.

UIDocument implements the NSFilePresenter protocol for you and does its work in the background, so the application is not blocked when opening or saving files, and the user can continue working with it. Such a behavior is enabled by a double queue architecture.

The first queue, the main thread of the application, is the one where your code is executed. Here you can open, close and edit files. The second queue is on the background and it is managed by UIKit. For example let’s say we want to open a document, which has been already created on iCloud.

Imagine we have an instance of UIDocument and we want to open a file stored in iCloud. We’d send a message like the following:


[doc openWithCompletionHandler:^(BOOL success) {
// Code to run when the open has completed
}];


This triggers a ‘read’ message into the background queue. You can’t call this method directly, for it gets called when you execute openWithCompletionHandler. Such an operation might take some time (for example, the file might be very big, or not downloaded locally yet).

In the meantime we can do something else on the UI so the application is not blocked. Once the reading is done we are free to load the data returned by the read operation.

This is exactly where UIDocument comes in handy, because you can override the loadFromContents:ofType:error method to read the data into your UIDocument subclass. Here’s a simplified version what it will look like for our simple notes app:


- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName
error:(NSError **)outError
{   
self.noteContent = [[NSString alloc]
initWithBytes:[contents bytes]
length:[contents length]
encoding:NSUTF8StringEncoding];          
return YES;   
}


This method is called by the background queue whenever the read operation has been completed.

The most important parameter here is contents, which is typically an NSData containing the actual data which you can use to create or update your model. You’d typically override this method to parse the NSData and pull out your document’s information, and store it in some instance variables in your UIDocument subclass, like shown here.

After loadFromContents:ofType:error completes, you’ll receive the callback you provided in the openWithCompletionHandler: block, as shown in the diagram below:

Order of operations when loading an UIDocument

To sum up, when you open a file you receive two callbacks: first in your UIDocument subclass when data has been read, and secondly when open operation is completely finished.

The write operation is pretty similar and it exploits the same double queue. The difference is that when opening a file we have to parse an NSData instance, but while writing we have to convert our document’s data to NSData and provide it to the background queue.

To save a document, you can either manually initiate the process by writing code, or via the autosaving feature implemented in UIDocument (more on this below).

If you want to save manually, you’d call a method like this:


[doc saveToURL:[doc fileURL]
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
// Code to run when the save has completed
}];


Just like when opening a file, there is a completion handler which is called when the writing procedure is done.

When asked to write the background queue asks for a snapshot of the contents of our UIDocument subclass. This is accomplished by overriding another method of UIDocument – contentsForType:error.

Here you should return an NSData instance which describes the current model to be saved. In our notes application, we’ll be returning an NSData representation of a string as follows:


- (id)contentsForType:(NSString *)typeName error:(NSError **)outError
{
return [NSData dataWithBytes:[self.noteContent UTF8String]
length:[self.noteContent length]];

}


The rest is taken care of in the background queue, which manages the storing of data. Once done the code in the completion handler will be executed.

For sake of completeness we should mention that in both reading and writing, instead of NSData you can use NSFileWrapper. While NSData is meant to manage flat files NSFileWrapper can handle packages, that is a directory with files treated as a single file. We’ll cover using NSFileWrapper later in this tutorial.

As we mentioned earlier, the save operation can be called explicitly via code or triggered automatically. The UIDocument class implements a saveless model, where data is saved automatically at periodic intervals or when certain events occur. This way there is no need for the user to tap a ‘save’ button anymore, because the system manages that automatically, e.g. when you switch to another document.

Under the hood the UIKit background queue calls a method on UIDocument called hasUnsavedChanges which returns whether the document is “dirty” and needs to be saved. In case of positive response the document is automatically saved. There is no way to directly set the value for such a method but there are two ways to influence it.

The first way is to explicitly call the updateChangeCount: method. This notifies the background queue about changes. As an alternative you can use the undo manager which is built in the UIDocument class. Each instance of this class (or subclasses) has in fact a property undoManager. Whenever a change is registered via an undo or redo action the updateChangeCount: is called automatically.

It is important to remember that in either case the propagation of changes might not be immediate. By sending these messages we are only providing ‘hints’ to the background queue, which will start the update process when it’s appropriate according to the device and the type of connection.

Subclassing UIDocument


Now that you have a good overview of UIDocument, let’s create a subclass for our note application and see how it works!

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Note, and make it a subclass of UIDocument.

To keep things simple, our class will just have a single property to store the note as a string. To add this, replace the contents of Note.h with the following:


#import <UIKit/UIKit.h>

@interface Note : UIDocument

@property (strong) NSString * noteContent;

@end


As we have learned above we have two override points, one when we read and one when we write. Add the implementation of these by replacing Note.m with the following:


#import "Note.h"

@implementation Note

@synthesize noteContent;

// Called whenever the application reads data from the file system
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName
error:(NSError **)outError
{

if ([contents length] > 0) {
self.noteContent = [[NSString alloc]
initWithBytes:[contents bytes]
length:[contents length]
encoding:NSUTF8StringEncoding];       
} else {
// When the note is first created, assign some default content
self.noteContent = @"Empty";
}

return YES;   
}

// Called whenever the application (auto)saves the content of a note
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError
{

if ([self.noteContent length] == 0) {
self.noteContent = @"Empty";
}

return [NSData dataWithBytes:[self.noteContent UTF8String]
length:[self.noteContent length]];

}

@end


When we load a file we need a procedure to ‘transform’ the NSData contents returned by the background queue into a string. Conversely, when we save we have to encode our string into an NSData object. In both cases we do a quick check and assign a default value in case the string is empty. This happens the first time that the document is created.

Believe it or not, the code we need to model the document is already over! Now we can move to the code related to loading and updating.

Opening an iCloud File


First of all we should decide a file name for our document. For this tutorial, we’ll start by creating a single filename. Add the following #define at the top of AppDelegate.m:


#define kFILENAME @"mydocument.dox"


Next, let’s extend the application delegate to keep track of our document, and a metadata query to look up the document in iCloud. Modify AppDelegate.h to look like the following:


#import <UIKit/UIKit.h>
#import "Note.h"

@class ViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong) Note * doc;
@property (strong) NSMetadataQuery *query;

- (void)loadDocument;

@end


Then switch to AppDelegate.m and synthesize the new propeties:


@synthesize doc = _doc;
@synthesize query = _query;


We’ve already added code into application:didFinishLaunchingWithOptions to check for the availability of iCloud. If iCloud is available, we want to call the new method we’re about to write to load our document from iCloud, so add the following line of code right after where it says “TODO: Load document”:


[self loadDocument];


Next we’ll write the loadDocument method. Let’s put it together bit by bit so we can discuss all the code as we go.


- (void)loadDocument {

NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
_query = query;

}


Note that before we can load a document from iCloud, we first have to check what’s there. Remember that we can’t simply enumerate the local directory returned to us by URLForUbiquityContainerIdentifier, because there may be files in iCloud not yet pulled down locally.

If you ever worked with Spotlight on the Mac, you’ll be familiar with the class NSMetadataQuery. It is a class to represent results of a query related to the properties of an object, such as a file.

In building such a query you have the possibility to specify parameters and scope, i.e. what you are looking for and where. In the case of iCloud files the scope is always NSMetadataQueryUbiquitousDocumentsScope. You can have multiple scopes, so we have to build an array containing just one item.

So continue loadDocument as follows:


- (void)loadDocument {

NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
_query = query;
[query setSearchScopes:[NSArray arrayWithObject:
NSMetadataQueryUbiquitousDocumentsScope]];

}


Now you can provide the parameters of the query. If you ever worked with CoreData or even arrays you probably know the approach. Basically, you build a predicate and set it as parameter of a query/search.

In our case we are looking for a file with a particular name, so the keyword is NSMetadataItemFSNameKey, where ‘FS’ stands for file system. Add the code to create and set the predicate next:


- (void)loadDocument {

NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
_query = query;
[query setSearchScopes:[NSArray arrayWithObject:
NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *pred = [NSPredicate predicateWithFormat:
@"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
[query setPredicate:pred];

}


You might not have seen the %K substitution before. It turns out predicates treat formatting characters a bit differently than you might be used to with NSString’s stringWithFormat. When you use %@ in predicates, it wraps the value you provide in quotes. You don’t want this for keypaths, so you use %K instead to avoid wrapping it in quotes. For more information, see the Predicate Format String Syntax in Apple’s documentation.

Now the query is ready to be run, but since it is an asynchronous process we need to set up an observer to catch a notification when it completes.

The specific notification we are interested in has a pretty long (but descriptive) name: NSMetadataQueryDidFinishGatheringNotification. This is posted when the query has finished gathering info from iCloud.

So here is the final implementation of our method:


- (void)loadDocument {

NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
_query = query;
[query setSearchScopes:[NSArray arrayWithObject:
NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *pred = [NSPredicate predicateWithFormat:
@"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
[query setPredicate:pred];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(queryDidFinishGathering:)
name:NSMetadataQueryDidFinishGatheringNotification
object:query];

[query startQuery];

}


Now that this is in place, add the code for the method that will be called when the query completes:


- (void)queryDidFinishGathering:(NSNotification *)notification {

NSMetadataQuery *query = [notification object];
[query disableUpdates];
[query stopQuery];

[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSMetadataQueryDidFinishGatheringNotification
object:query];

_query = nil;

[self loadData:query];

}


Note that once you run a query, if you don’t stop it it runs forever or until you quit the application. Especially in a cloud environment things can change often. It might happen that while you are processing the results of a query, due to live updates, the results change! So it is important to stop this process by calling disableUpdates and stopQuery. In particular the first prevents live updates and the second allows you to stop a process without deleting already collected results.

We then remove ourselves as an observer to ignore further notifications, and finally call a method to load the document, passing the NSMetadataQuery as a parameter.

Add the starter implementation of this method next (add this above queryDidFinishGathering):


- (void)loadData:(NSMetadataQuery *)query {

if ([query resultCount] == 1) {
NSMetadataItem *item = [query resultAtIndex:0];

}
}


As you can see here, a NSMetadataQuery wraps an array of NSMetadataItems which contain the results. In our case, we are working with just one file so we are just interested in the first element.

An NSMetadataItem is like a dictionary, storing keys and values. It has a set of predefined keys that you can use to look up information about each file:

  • NSMetadataItemURLKey
  • NSMetadataItemFSNameKey
  • NSMetadataItemDisplayNameKey
  • NSMetadataItemIsUbiquitousKey
  • NSMetadataUbiquitousItemHasUnresolvedConflictsKey
  • NSMetadataUbiquitousItemIsDownloadedKey
  • NSMetadataUbiquitousItemIsDownloadingKey
  • NSMetadataUbiquitousItemIsUploadedKey
  • NSMetadataUbiquitousItemIsUploadingKey
  • NSMetadataUbiquitousItemPercentDownloadedKey
  • NSMetadataUbiquitousItemPercentUploadedKey

In our case, we are interested in NSMetadataItemURLKey, which points to the URL that we need to build our Note instance. Continue the loadData method as follows:


- (void)loadData:(NSMetadataQuery *)query {

if ([query resultCount] == 1) {

NSMetadataItem *item = [query resultAtIndex:0];
NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
Note *doc = [[Note alloc] initWithFileURL:url];
self.doc = doc;

}
}


When you create a UIDocument (or a subclass of UIDocument like Note), you always have to use the initWithFileURL initializer and give it the URL of the document to open. We call that here, pasing in the URL of the located file, and store it away in an instance variable.

Now we are ready to open the note. As explained previously you can open a document with the openWithCompletionHandler method, so continue loadData as follows:


- (void)loadData:(NSMetadataQuery *)query {

if ([query resultCount] == 1) {

NSMetadataItem *item = [query resultAtIndex:0];
NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
Note *doc = [[Note alloc] initWithFileURL:url];
self.doc = doc;
[self.doc openWithCompletionHandler:^(BOOL success) {
if (success) {               
NSLog(@"iCloud document opened");                   
} else {               
NSLog(@"failed opening document from iCloud");               
}
}];

}
}


You can run the app now, and it seems to work… except it never prints out either of the above messages! This is because there is currently no document in our container in iCloud, so the search isn’t finding anything (and the result count is 0).

Since the only way to add a document on the iCloud is via an app, we need to write some code to create a doc. We will append this to the loadData method that we defined a few seconds ago. When the query returns zero results, we should:

  • Retrieve the local iCloud directory
  • Initialize an instance of document in that directory
  • Call the saveToURL method
  • When the save is successful we can call openWithCompletionHandler.

So add an else case to the if statement in loadData as follows:


else {

NSURL *ubiq = [[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil];
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:
@"Documents"] URLByAppendingPathComponent:kFILENAME];

Note *doc = [[Note alloc] initWithFileURL:ubiquitousPackage];
self.doc = doc;

[doc saveToURL:[doc fileURL]
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {           
if (success) {
[doc openWithCompletionHandler:^(BOOL success) {               
NSLog(@"new document opened from iCloud");               
}];               
}
}];
}


Compile and run your app, and you should see the “new document” message arrive the first time you run it, and “iCloud document opened” in subsequent runs.

You can even try this on a second device (I recommend temporarily commenting out the else case first though to avoid creating two documents due to timing issues), and you should see the “iCloud document opened” message show up on the second device (because the document already exists on iCloud now!)

Now our application is almost ready. The iCloud part is over, and we just need to set up the UI – which we’ll continue in the next part of the series!

Where To Go Form Here?


At this point, you should have some basic experience working with iCloud. But stay tuned for the next part to the tutorial, where we’ll add a UI onto the app, and cover how extend our app to work with multiple documents!

This tutorial is a sample chapter from our new book iOS 5 By Tutorials. If you like this tutorial, you might want to check out the book, because the final version will include an entire additional chapter on iCloud not posted here!

If you have any comments or questions on this tutorial or on iCloud in general, please join the forum discussion below!

This is a post by iOS Tutorial Team member Cesare Rocchi, a UX designer and developer specializing in web and mobile applications. He runs Studio Magnolia, an interactive studio that creates compelling web and mobile applications.

Beginning iCloud in iOS 5 Tutorial Part 1 is a post from: Ray Wenderlich

2011年10月29日星期六

Working with JSON in iOS 5 Tutorial

Working with JSON in iOS 5 Tutorial:
Learn how you can easily read and write JSON in iOS 5!
Learn how you can easily read and write JSON in iOS 5!

Note from Ray: This is the eighth iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 By Tutorials. Enjoy!

This is a blog post by iOS Tutorial Team member Marin Todorov, a software developer with 12+ years of experience, an independent iOS developer and the creator of Touch Code Magazine.

iOS 5 has some new built-in APIs to make it really easy to read and write JSON.

If you don’t know what JSON is, it’s a simple human readable format that is often used to send data over a network connection.

For example, if you have an array of three strings, the JSON representation would simply be:

["test1", "test2", "test3"]

If you have a Pet object with member variables name, breed, and age, the JSON representation would simply be:

{"name" : "Dusty", "breed": "Poodle", "age": 7}

It’s that simple, which is why it’s so easy and popular to use. For the full spec, which can be read in just a couple minutes, check out www.json.org.

The reason JSON is important is that many third parties such as Google, Yahoo, or Kiva make web services that return JSON formatted data when you visit a URL with a specified query string. If you write your own web service, you’ll also probably find it really easy to convert your data to JSON when sending to another party.

In this tutorial, we’ll cover how you can work with JSON in iOS 5. Keep reading to see how simple it is!



JSON and iOS 5


If you’ve had to parse JSON in your iOS apps in the past, you’ve probably used a third party library such as JSON Framework.

Well with iOS 5, needing to use a third party library for JSON parsing is a thing of the past. Apple has finally added a JSON library in Cocoa and I must say I personally like it very much!

You can turn objects like NSString, NSNumber, NSArray and NSDictionary into JSON data and vice versa super easily. And of course no need to include external libraries – everything is done natively and super fast.

In this chapter we’re going to get hands-on experience with the new native JSON support. We’re going to build a simple application which will connect to the Internet and consume JSON service from the Kiva.org web site. It will parse the data, show it in human readable format on the screen, and then build back a different JSON.

descr

Later on in the chapter we’re going to create a class category which will give you ideas how to integrate JSON support more tightly into Cocoa and your own classes. Having the possibility to turn your own classes data into JSON could really help you persist data structures online, exchange data between applications, or anything that requires your classes to be able to serialize and persist data, which can be sent over http, email, etc.

Getting Started


Open up Xcode and from the main menu choose File\New\New Project. Choose the iOS\Application\Single View Application template, and click Next. Name the product KivaJSONDemo, select iPhone for the Device family, and make sure just the Use Automatic Reference Counting checkbox is checked, click Next and save the project by clicking Create.

Let’s do a little bit of clean up first – open up ViewController.m file and replace everything inside with this :


#define kBgQueue dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) //1
#define kLatestKivaLoansURL [NSURL URLWithString: 
@"http://api.kivaws.org/v1/loans/search.json?status=fundraising"] //2

#import "ViewController.h"

@implementation ViewController

@end


In the first line of code we define a macro that gives us back a background queue – I like having a kBgQueue shortcut for that, so I can keep my code tighter.

In the second line of code we create a macro named kLatestKivaLoansURL which returns us an NSURL pointing to this URL [http://api.kivaws.org/v1/loans/search.json?status=fundraising].

Go ahead and visit this URL in your browser if you want – you’ll see Kiva.org’s list of currently fundraising loans in JSON format. We’re going to use this API to read the list of loans, take the latest one and show the information on the screen.

Let’s make a little detour and design the application’s UI in Interface Builder real quick.

Open ViewController.xib in the Project Navigator. This app is supposed to be positive and life-changing, so we need to do something about that background! Select it and from the Utilities bar (1), make sure you have the Attributes Inspector open (2), and set the Background to a nice green color (3).

descr

Now grab a label from the Object library and drop it inside the already open view (1). Resize the label so it fits about 4 lines of text and takes almost the screen’s width (2). Then from the Attributes Inspector make the following changes: set “Background” to white, set “Lines” to “5″ (3). Click on the label to make sure it’s selected and then press Cmd+C, Cmd+V to clone the label (4). Finally arrange the two labels like on the screenshot:

descr

To polish up the interface add 3 more labels and finish the UI so it looks like this:

descr

The only thing left is to connect our labels to a couple of IBOutlets in our class. Switch to ViewController.h and add two instance variables inside the interface:


@interface ViewController : UIViewController {
IBOutlet UILabel* humanReadble;
IBOutlet UILabel* jsonSummary;
}


Then open up ViewController.xib again. Control-drag from “File’s owner” onto the 1st 5-line label and from the popup menu choose humanReadable.

Again while holding the “ctrl” key drag with the mouse from “File’s owner” onto the 2nd 5-line label and from the popup menu choose jsonSummary.

That concludes the project setup – we’re ready to start coding!

Parsing JSON from the Web


The first thing we need to do is download the JSON data from the web. Luckily, with GCD we can do this in one line of code! Add the following to ViewController.m:


- (void)viewDidLoad
{
[super viewDidLoad];

dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:
kLatestKivaLoansURL];
[self performSelectorOnMainThread:@selector(fetchedData:)
withObject:data waitUntilDone:YES];
});
}


Remember how earlier we defined kBGQueue as a macro which gives us a background queue?

Well this bit of code makes it so that when viewDidLoad is called, we run a block of code in this background queue to download the contents at the Kiva loans URL.

When NSData has finished fetching data from the Internet we call performSelectorOnMainThread:withObject:waitUntilDone: so we can update the application’s UI. We haven’t written fetchedData: yet but will do so shortly.

Remember it is only OK to run a synchronous method such as dataWithContentsOfURL in a background thread, otherwise the GUI will seem unresponsive to the user.

Also, remember that you can only access UIKit objects from the main thread, which is why we had to run fetchedData: on the main thread.

Note: You might wonder why I preferred to use performSelectorOnMainThread:withObject:waitUntilDone: over dispatching a block on the main thread? It’s a personal preference really and I have two reasons:

I’m all for the greatest readability of a piece of code. For me, [self performSelectorOnMainThread:...] makes it easier to spot what’s going on in that piece of code.

I’m a symmetry freak! I find that Xcode doesn’t handle text indentation well when you use dispatch_async(), so purely visually the code is not so pleasant to look at.

You might have other preferences, so yes – if you prefer dispatch_async(dispatch_get_main_queue(), ^(){…}); go for it!

So, when the data has arrived the method fetchedData: will be called and the NSData instance will be passed to it. In our case the JSON file is relatively small so we’re going to do the parsing inside fetchedData: on the main thread. If you’re parsing large JSON feeds (which is often the case), be sure to do that in the background.

So next add the fetchedData method to the file:


- (void)fetchedData:(NSData *)responseData {
//parse out the json data
NSError* error;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseData //1

options:kNilOptions
error:&error];

NSArray* latestLoans = [json objectForKey:@"loans"]; //2

NSLog(@"loans: %@", latestLoans); //3
}


This is it – the new iOS 5 JSON magic!

Basically iOS 5 has a new class named NSJSONSerialization. It has a static method called JSONObjectWithData:options:error that takes an NSData and gives you back a Foundation object – usually an NSDictionary or an NSArray depending what do you have at the top of your JSON file hierarchy.

In Kiva.org’s case at the top there’s a dictionary, which has a key with list of loans. In line 1, we get an NSDictionary from the JSON data. In line 2, we get an NSArray latestLoans which is the loans key in the top JSON dictionary.

Finally in line 3, we dump latestLoans to the console, so we’re sure everything’s OK. Hit Run and check Xcode’s console to see the result:

descr

Not bad for 3 lines of code, eh? :]

Parsing Options


I’d like to talk just a bit more about NSJSONSerialization’s JSONObjectWithData:options:error: method. It’s one of these Apple APIs which understand and do everything by themselves, but you still can configure a bit its behavior.

Notice in the code that I chose to pass for the parameter options a value of kNilOptions. kNilOptions is just a constant for 0 – I find its name very descriptive though, so I always prefer it over just the value of 0 as a method parameter.

However you can pass other values or even a bit mask of values to combine them. Have a look at what you got as options when you’re converting JSON to objects:

  • NSJSONReadingMutableContainers: The arrays and dictionaries created will be mutable. Good if you want to add things to the containers after parsing it.
  • NSJSONReadingMutableLeaves: The leaves (i.e. the values inside the arrays and dictionaries) will be mutable. Good if you want to modify the strings read in, etc.
  • NSJSONReadingAllowFragments: Parses out the top-level objects that are not arrays or dictionaries.

So, if you’re not only reading, but also modifying the data structure from your JSON file, pass the appropriate options from the list above to JSONObjectWithData:options:error:.

Displaying to the Screen


We’re going to continue by showing the latest loan information on the screen. At the end of “fetchedData:” method add these few lines of code:


// 1) Get the latest loan
NSDictionary* loan = [latestLoans objectAtIndex:0];

// 2) Get the funded amount and loan amount
NSNumber* fundedAmount = [loan objectForKey:@"funded_amount"];
NSNumber* loanAmount = [loan objectForKey:@"loan_amount"];
float outstandingAmount = [loanAmount floatValue] -
[fundedAmount floatValue];

// 3) Set the label appropriately
humanReadble.text = [NSString stringWithFormat:@"Latest loan: %@
from %@ needs another $%.2f to pursue their entrepreneural dream",
[loan objectForKey:@"name"],
[(NSDictionary*)[loan objectForKey:@"location"]
objectForKey:@"country"],
outstandingAmount];


The latestLoans array is a list of dictionaries, so (1) we get the first (and latest) loan dictionary and (2) we fetch few values about the loan. Finally (3) we set the text of the 1st label in the UI.

OK! Let’s have a look – hit Run and see what comes up:

descr

Of course the information you see will be different as Kiva adds loans constantly – but it’s clear we achieved what we wanted, we parsed JSON data and visualized some human readable info.

Generating JSON Data


Now let’s do the opposite. From the loan NSDictionary that we now have we’ll build some JSON data, which we will be able to send over to a server, another app, or do with it whatever else we want.

Add this code to the end of the fetchedData: method:


//build an info object and convert to json
NSDictionary* info = [NSDictionary dictionaryWithObjectsAndKeys:
[loan objectForKey:@"name"],
@"who",
[(NSDictionary*)[loan objectForKey:@"location"]
objectForKey:@"country"],
@"where",
[NSNumber numberWithFloat: outstandingAmount],
@"what",
nil];

//convert object to data
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:info
options:NSJSONWritingPrettyPrinted error:&error];


Here we build an NSDictionary called info where we store the loan information as who, where, and what in different keys and values.

Then we call dataWithJSONObject:options:error: – the opposite to the JSON API we just used before. It takes in an object and turns it into JSON data.

For the options parameter there’s only one possible value – NSJSONWritingPrettyPrinted. If you want to send the JSON over the Internet to a server use kNilOptions as this will generate compact JSON code, and if you want to see the JSON use NSJSONWritingPrettyPrinted as this will format it nicely.

So, at this point our job of turning info into JSON is finished, but we can’t be sure before we see that it is actually so. Let’s show the JSON into our second UI label. Add this final line of code to fetchedData:


//print out the data contents
jsonSummary.text = [[NSString alloc] initWithData:jsonData                                       
encoding:NSUTF8StringEncoding];


By initializing an NSString with initWithData:encoding: we easily get the text representation of our JSON data and we show it straight inside the jsonSummary label. Hit Run and:

descr

Integrating Objects and JSON


Imagine if NSDictionary, NSArray, NSString, and NSData had methods to convert to and from JSON data – wouldn’t that be great?

Oh, but wait – we’re using it’s Objective-C, so we can actually extend foundation classes with methods of our own! Let’s do an example with NSDictionary and see how useful that could be.

Open ViewController.m, and add this category just above the @implementation:


@interface NSDictionary(JSONCategories)
+(NSDictionary*)dictionaryWithContentsOfJSONURLString:
(NSString*)urlAddress;
-(NSData*)toJSON;
@end

@implementation NSDictionary(JSONCategories)
+(NSDictionary*)dictionaryWithContentsOfJSONURLString:
(NSString*)urlAddress
{
NSData* data = [NSData dataWithContentsOfURL:
[NSURL URLWithString: urlAddress] ];
__autoreleasing NSError* error = nil;
id result = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions error:&error];
if (error != nil) return nil;
return result;
}

-(NSData*)toJSON
{
NSError* error = nil;
id result = [NSJSONSerialization dataWithJSONObject:self
options:kNilOptions error:&error];
if (error != nil) return nil;
return result;   
}
@end


As there’s nothing new that I didn’t speak about so far in this tutorial I won’t go over the code line by line.

But basically, we define 2 methods on NSDictionary: one dictionaryWithContentsOfJSONURLString: which gets an NSString with a web address (it’s often easier to work with URLs as text, not as NSURL instances), does all the downloading, fetching, parsing and whatnot and finally just returns an instance of a dictionary (or nil in case of an error) – ain’t that pretty handy?

In the category there’s also one more method – toJSON which you call on an NSDictionary instance to get JSON data out of it.

So with this category fetching JSON from the web becomes as easy as :


NSDictionary* myInfo =
[NSDictionary dictionaryWithContentsOfJSONURLString:
@"http://www.yahoo.com/news.json"];


And of course on any of your NSDictionary objects you can do:


NSDictionary* information =
[NSDictionary dictionaryWithObjectsAndKeys:
@"orange",@"apple",@"banana",@"fig",nil];
NSData* json = [information toJSON];


Pretty cool and readable code. Now of course you can also extend NSMutableDictionary with the same dictionaryWithContentsOfJSONURLString: method, but in there you’ll have to pass NSJSONReadingMutableContainers as options – so hey, NSMutableDictionary could be initialized with JSON too, and it’ll hold mutable data. Cool!

Where to Go From Here?


Here is a example project with all of the code from the above tutorial.

At this point, you have hands-on experience with the awesome new iOS5 JSON reading and writing APIs, and are ready to start using this in your own apps!

Before we go though, I want to mention just few more methods from the NSJSONSerialization class.


BOOL isTurnableToJSON = [NSJSONSerialization
isValidJSONObject: object]


As you might guess, isValidJSONObject: tells you whether you can successfully turn a Cocoa object into JSON data.

Also I presented to you the 2 methods to read and write JSON from/to NSData objects, but you can do that also on streams – with JSONObjectWithStream:options:error: and writeJSONObject:toStream:options:error:, so do have a look at the class documentation.

If you want to keep playing around with JSON, feel free to extend the demo project with the following features:

  • Modify the demo project to use the JSON categories, like we discussed above

    Develop further JSON categories for NSArray, NSString, etc
  • Think about how cool it’d be if your classes had a toJSON method – so you can easily persist them on your web server!
  • Make an implementation on a test class to see if you can get it working!

This is an example of one of the “bonus” chapters from our new book iOS 5 By Tutorials. The bonus chapters like this one cover some of the cool but easily overlooked new APIs in iOS 5 like the new Address Book APIs, new Location APIs, new Calendar APIs, and much more, so if you want to learn more about iOS 5 check it out!

If you have any questions, comments, or suggestions, please join in the forum discussion below!

This is a blog post by iOS Tutorial Team member Marin Todorov, a software developer with 12+ years of experience, an independant iOS developer and the creator of Touch Code Magazine.

Working with JSON in iOS 5 Tutorial is a post from: Ray Wenderlich