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:
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:
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.
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.
Loading Notes
To load notes, we’ll follow a similar strategy to what we did earlier when loading a single note. However, this time we don’t know the exact file name, so we have to tweak our search predicate to look for a file name like “Note_*”.
So add this new method to ListViewController.m:
- (void)loadNotes { NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil]; if (ubiq) { self.query = [[NSMetadataQuery alloc] init]; [self.query setSearchScopes: [NSArray arrayWithObject: NSMetadataQueryUbiquitousDocumentsScope]]; NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K like 'Note_*'", NSMetadataItemFSNameKey]; [self.query setPredicate:pred]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:self.query]; [self.query startQuery]; } else { NSLog(@"No iCloud access"); } } |
We might be tempted to place a call to this loadNotes method in viewDidLoad. That would be correct, but that is executed on initial app startup. If we want to reload data really each time the app is opened (even from the background), it’s better to add an observer to listen when the application becomes active.
So add the following line of code to the end of viewDidLoad:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadNotes) name: UIApplicationDidBecomeActiveNotification object:nil]; |
This calls the loadNotes method we just wrote on statup (or when the application returns from the background). And we set up loadNotes to call queryDidFinishGathering when the metadata search completes, so add the code for that next:
- (void)queryDidFinishGathering:(NSNotification *)notification { NSMetadataQuery *query = [notification object]; [query disableUpdates]; [query stopQuery]; [self loadData:query]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query]; self.query = nil; } |
This is exactly the same as we added earlier in the tutorial. Next, add the implementation of loadData as follows (right above queryDidFinishGathering):
- (void)loadData:(NSMetadataQuery *)query { [self.notes removeAllObjects]; for (NSMetadataItem *item in [query results]) { NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; Note *doc = [[Note alloc] initWithFileURL:url]; [doc openWithCompletionHandler:^(BOOL success) { if (success) { [self.notes addObject:doc]; [self.tableView reloadData]; } else { NSLog(@"failed to open from iCloud"); } }]; } } |
The loadData method has to populate the array of notes according to the results of the query. The implementation here fully reloads the list of notes as returned by iCloud.
One final change. For testing purposes, go ahead and add a ‘refresh’ button to the navigator item which triggers the loadNotes method when tapped, by adding this code to the bottom of viewDidLoad:
UIBarButtonItem *refreshItem = [[UIBarButtonItem alloc] initWithTitle:@"Refresh" style:UIBarButtonItemStylePlain target:self action:@selector(loadNotes)]; self.navigationItem.leftBarButtonItem = refreshItem; |
That’s it! Compile and run the app on one device, and create a few notes. Then start the app on another device, and see that the table is correctly populated with the same notes!
Where To Go From Here?
Here is a example project with all of the code from the above tutorial.
Congratulations, you now have hands-on experience with the basics of using iCloud and the new UIDocument class, to create an iCloud-enabled app with multi-document support.
We’ve just scratched the surface of iCloud. If you are interested in learning more about iCloud, check our our book iOS 5 By Tutorials – the final version will have an additional chapter that covers handling conflict resolution, using NSFileWrapper, storing simple key-value pairs, and using Core Data with iCloud!
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 2 is a post from: Ray Wenderlich
没有评论:
发表评论