Beginning iCloud in iOS 5 Tutorial Part 2

Note from Ray: This is the tenth 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. This is […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share

Note from Ray: This is the tenth 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.

This is the second part of a two-part tutorial series on how to get started using iCloud in iOS 5.

In the first part of the tutorial series, we covered how iCloud works and how to open and save a UIDocument programatically.

In this part of the tutorial, we’ll add a user interface to our app and cover how to work with multiple documents.

This tutorial continues where the first part left off, so be sure to go through it first.

Setting Up the User Interface

The Xcode project template we chose already set up an empty view controller for us. We will extend it by adding the current document and a UITextView to display the content of our note.

Start by modifying ViewController.h to look like the following:

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

@interface ViewController : UIViewController <UITextViewDelegate>

@property (strong) Note * doc;
@property (weak) IBOutlet UITextView * noteView;

@end

We have also marked the view controller as implementing UITextViewDelegate so that we can receive events from the text view.

Next, open up ViewController_iPhone.xib and make the following changes:

  • Drag a Text View into the View, and make it fill the entire area.
  • Control-click the File’s Owner, and drag a line from the noteView outlet to the Text View.
  • Control-click the Text View, and drag a line from the delegate to the File’s Owner.

At this point your screen should look like this:

Adding a text view into Interface Builder

When you are done, repeat these steps for ViewController_iPad.xib as well.

Next, open up ViewController.m and synchronize your new properties as follows:

@synthesize doc;
@synthesize noteView;

Then modify viewDidLoad to register for the notification our code will send when our document changes (we’ll add the code to send this notification later):

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(dataReloaded:) 
        name:@"noteModified" object:nil];
}

Next, implement the method that gets called when the notification is received as follows:

- (void)dataReloaded:(NSNotification *)notification {
    
    self.doc = notification.object;
    self.noteView.text = self.doc.noteContent;
    
}

This simply stores the current document and updates the text view according to the new content received.

In general substituting the old content with the new one is NOT a good practice. When we receive a notification of change from iCloud we should have a conflict resolution policy to enable the user to accept/refuse/merge the differences between the local version and the iCloud one. We’ll discuss more about conflict resolution later, but for now to keep things simple we’ll just overwrite each time.

Next, implement textViewDidChange to notify iCloud when the document changes, and modify the app to refresh the data in viewWillAppear as well:

- (void)textViewDidChange:(UITextView *)textView {
    
    self.doc.noteContent = textView.text;
    [self.doc updateChangeCount:UIDocumentChangeDone];
    
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.noteView.text = self.doc.noteContent;
}

As above this is not a great practice, because we are going to notify each iCloud about every single change (i.e. each time a character is added or deleted). For efficiency, it would be better to just tell iCloud every so often, or when the user has finished a batch of edits.

There’s just one last step remaining – we need to add the code to send the “noteModified” notification we registered for in viewDidLoad. The best place in this case is the Note class’s loadFromContents:ofType:error, method which is called whenever data are read from the cloud.

So open up Note.m and add this line of code to the bottom of loadFromContents:ofType:error (before the return YES):

[[NSNotificationCenter defaultCenter] 
    postNotificationName:@"noteModified" 
    object:self];

Now we are really ready! The best way to test this application is the following: install it on two devices and run it on both. You should be able to edit on one and see the changes periodically propagated to the other.

The propagation of changes is not immediate and might depend on your connectivity. In general, for our examples, it should take 5-30 seconds. Another way to check the correctness it to browse the list of files in your iCloud.

It is a bit hidden in the menu. Here is the sequence:

Settings -> iCloud -> Storage and Backup -> Manage Storage -> Documents & Data -> Unknown

If the application works correctly you should see the note we created in our app:

iCloud document for app in Settings

The ‘unknown’ label comes from the fact that the application has not been uploaded and approved on the Apple Store yet.

Also note that users can delete files from iCloud from this screen at-will (without having to go through your app). So keep this in mind as you’re developing.

Congrats – you have built your first iCloud-aware application!

Handling Multiple Documents

Cool, our example works and we are a bit more acquainted with the capabilities of iCloud. But what we have right now isn’t enough to impress our users, or build an application that makes sense. Who wants to manage just one document?!

So next we are going to extend our application to manage more than one document at a time. The most natural development of our current prototype is to transform it into a notes application, as follows:

  • The application will start with a view showing a list of notes
  • Each note will have a unique id
  • Tapping a note will show a single note view with the content
  • Users can then edit the content
  • The list of notes is updated when we launch the application or tap a refresh button

We will reuse some of the code of the previous project but we will need to reorganize it. Let’s start by rearranging the user interface.

Reorganizing the User Interface

In our new project a single view is not enough, we’ll need two. The first will be a table view which shows the list of notes. The second will be pretty similar to the main view of the previous project: it will show an editable text view.

New architecture for our app

Let’s add a empty table view controller, and modify our app to show that first inside a navigation controller.

Create a new file with the iOS\Cocoa Touch\UIViewController subclass template, name the class ListViewController, and make it a subclass of UITableViewController. You can leave both checkboxes unchecked.

Then open AppDelegate.m, and import ListViewController.h at the top of the file:

#import "ListViewController.h"

Then replace the first few lines of application:didFinishLaunchingWithOptions with the following:

self.window = 
    [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
ListViewController * listViewController = 
    [[ListViewController alloc] initWithNibName:nil bundle:nil];
UINavigationController * navController = 
    [[UINavigationController alloc] initWithRootViewController:
        listViewController];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible];

This sets the app up to start with a navigation controller, with the new ListViewController as the first thing inside.

You can compile and run at this point, and you’ll see an empty table. A good start, but let’s make the table show our iCloud docs! Modify ListViewController.h to the following:

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

@interface ListViewController : UITableViewController

@property (strong) NSMutableArray * notes;
@property (strong) ViewController * detailViewController;
@property (strong) NSMetadataQuery *query;

- (void)loadNotes;

@end

Here we’ve added an array to store the notes, a reference to the old view controller we created we’ll be pushing onto the stack, a metadata query we’ll use to load the notes, and a loadNotes method we’ll write later.

Next switch over to ListViewController.m and synthesize properties at the top of the file:

@synthesize notes = _notes;
@synthesize detailViewController = _detailViewController;
@synthesize query = _query;

Then add the following code to the bottom of viewDidLoad:

self.notes = [[NSMutableArray alloc] init];
self.title = @"Notes";
UIBarButtonItem *addNoteItem = [[UIBarButtonItem alloc] 
    initWithTitle:@"Add" 
    style:UIBarButtonItemStylePlain
    target:self 
    action:@selector(addNote:)];
self.navigationItem.rightBarButtonItem = addNoteItem;

This initializes the notes array to an empty list, sets up a title for this view controller, and adds a button to the navigation bar that says “Add”. When the user taps this, the addNote method will be called, and we’ll implement this to add a new note.

Unlike the previous project that had just one document (so always used the same filename each time), this time we’re storing multiple documents (one for each note created), so we need a way to generate unique file names. As an easy solution, we will use the creation date of the file and prepend the ‘Note_’ string.

So add the implementation of addNote as follows:


- (void)addNote:(id)sender {
    
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMdd_hhmmss"];
    
    NSString *fileName = [NSString stringWithFormat:@"Note_%@", 
                         [formatter stringFromDate:[NSDate date]]];
    
    NSURL *ubiq = [[NSFileManager defaultManager]
        URLForUbiquityContainerIdentifier:nil];
    NSURL *ubiquitousPackage = 
        [[ubiq URLByAppendingPathComponent:@"Documents"] 
            URLByAppendingPathComponent:fileName];
    
    Note *doc = [[Note alloc] initWithFileURL:ubiquitousPackage];
    
    [doc saveToURL:[doc fileURL] 
        forSaveOperation:UIDocumentSaveForCreating 
        completionHandler:^(BOOL success) {
        
        if (success) {
            
            [self.notes addObject:doc];
            [self.tableView reloadData];
            
        }
        
    }];
    
}

You should be pretty familiar with this code. The file name is generated by combining the current date and hour. We call the saveToURL method and, in case of success, we add the newly created note to the array which populates the table view.

Almost done with the ability to add notes – just need to add the code to populate the table view with the contents of the notes array. Implement the table view data source methods like the following:


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
    return self.notes.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] 
            initWithStyle:UITableViewCellStyleDefault 
            reuseIdentifier:CellIdentifier];
        cell.accessoryType = 
            UITableViewCellAccessoryDisclosureIndicator;
    }
    
    Note * note = [_notes objectAtIndex:indexPath.row];
    cell.textLabel.text = note.fileURL.lastPathComponent;
    
    return cell;
}

- (void)tableView:(UITableView *)tableView 
  didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        self.detailViewController = [[ViewController alloc] 
            initWithNibName:@"ViewController_iPad" bundle:nil];
    } else {
        self.detailViewController = [[ViewController alloc] 
            initWithNibName:@"ViewController_iPhone" bundle:nil];
    }
    Note * note = [_notes objectAtIndex:indexPath.row];
    self.detailViewController.doc = note;
    [self.navigationController 
        pushViewController:self.detailViewController animated:YES];
}

Compile and run the application, and you should be able to add new notes when you tap the Add button. You can also go to the iCloud manager in settings to verify they are actually in iCloud.

But what if we quit the application and restart it? The list is empty! We need in fact a way to load them at startup or when the application becomes active.

Contributors

Over 300 content creators. Join our team.