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

没有评论:

发表评论