Wednesday, July 18, 2012

How To Create A Simple iPhone App in iOS 5 Tutorial: Part 3/3


This article is the final part of a 3 part series on how to create a simple iPhone app for beginners. And this app happens to be about rating scary bugs!
In the first part of the series, we created an app that contained a list of bugs in a table view.
In the second part of the series, we covered how to create a detail view for the bugs.
In this article, we’ll cover how to add new bugs, how to add an icon and default image to our project, and how to handle long-running operations.
So let’s wrap this app up!

Adding and Deleting Bugs

Everything’s working great so far, but so far this isn’t a very user-friendly app! I mean the first thing anyone would want to do is add their own bug, and so far the only way to do that is by editing code!
Luckily, since we wrote our DetailViewController to allow editing of bugs, and are using a UITableViewController for the RootViewController, most of the infrastructure is already in place! There are just four changes we have to make, but I’m going to explain them bit-by-bit to keep things easy to understand:

1) Set up navigation bar buttons
Inside MasterViewController.m, add the following lines of code to viewDidLoad:
self.navigationItem.leftBarButtonItem = self.editButtonItem;
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self action:@selector(addTapped:)];
Here we set up some buttons in the navigation bar. Just like “title” is a special property in view controllers used by the navigation controller, “navigationItem” is another of those. Whatever you set for the leftBarButtonItem and the rightBarButtonItem will show up in the navigation bar when the navigation controller shows your view controller.
For the leftBarButtonItem, we use a special built-in button called “editButtonItem.” This button says “Edit” and toggles the UITableView between edit mode (where you can delete rows for example) and normal mode.
For the rightBarButtonItem, we create a button that the user can tap to create a new bug entry. It turns out there’s already a built-in system item for adding (that looks like a + symbol), so we go ahead and use that, and register the “addTapped:” method to be called when it’s tapped.
Note that you can also set these up in the Storyboard editor, but I set them up here in code to show you that you can do things this way too.
2) Implement tableView:commitEditingStyle:forRowAtIndexPath
Still in MasterViewController.m, uncomment tableView:commitEditingStyle:forRowAtIndexPath and replace the contents with the following:
if (editingStyle == UITableViewCellEditingStyleDelete) {        
[_bugs removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
This method is called when the user chooses to modify a row in some way. We check to see that the user is trying to delete the row, and if so we delete it. Note we have to remove it both from our data model (_bugs) AND notify the table view that one of the rows has been deleted, via deleteRowsAtIndexPaths.
3) Handle adding a new bug
Next add this new method to MasterViewController.m:
- (void)addTapped:(id)sender {
ScaryBugDoc *newDoc = [[ScaryBugDoc alloc] initWithTitle:@"New Bug" rating:0 thumbImage:nil fullImage:nil];
[_bugs addObject:newDoc];
 
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_bugs.count-1 inSection:0];
NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
 
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
[self performSegueWithIdentifier:@"MySegue" sender:self];
}
We set things up in step 1 so that when the user taps the add button, this method gets called.
Here we create a ScaryBugDoc with some default values, and add it to the bugs array. Note we have to also update the table view so it knows there’s a new row.
Then we call some code to make the table view act as-if the user selected the new row, so we immediately go into the edit view for the new bug. We first select the row in the table view, then we manually perform the segue that we set up in the Storyboard editor.
Note that we never actually named the segue “MySegue” in the Storyboard editor earlier, so let’s do that as our final step.
4) Name the segue
Open MainStoryboard.storyboard and click on the arrow with an icon between the master and detail view controller – this is your segue.
Then in the fourth tab on the sidebar (the Attributes Inspector), set the Identifier to MySegue. This way we can manually run it when we want from code.
Naming a segue in Xcode
That’s it! If you compile and run the code, you should now be able to add your own bugs, such as this one:
An Objective-C Bug

Adding An Icon and Default Image

Ok, our app is looking pretty fun and amusing, let’s ship it and get rich!
Except it would be pretty embarassing if we did right now, I mean we don’t even have an icon!
Luckily, that is quite easy to fix. Earlier, we added an icon to our project file (logo1.png), from ExtraStuffForScaryBugs2.zip. Let’s set that as the icon for our project!
To do that, the easiest way is to select your ScaryBugs project in the Project navigator, and select your ScaryBugs target. Make sure the Summary tab is selected, and scroll down to App Icons. Control-click on the first icon and choose Select File:
Changing the icon for an app in Xcode
Choose logo1.png from your project directory, accept any warnings, and you should see it show up. If you get a warning that the image size doesn’t match, resize logo1.png to 57×57 (an earlier download had the file larger than that).
Note that this is a shortcut for editing ScaryBugs-Info.plist, where your app settings like this are stored. You can verify this by opening ScaryBugs-Info.plist, and you should see the following:
Settings to set an app's icon in the info.plist
You could have manually added these settings to this file, but I find using the GUI easier when it’s available.
Go ahead and delete and re-install the app on your simulator or phone, and you should see the new icon show up!
New icon for Scary Bugs
There’s one other thing we should fix as well. If you try running ScaryBugs, you might notice that after you tap the icon, there’s a pause before it shows up where just a black screen displays. That is kind of embarassing behavior – it looks like the app isn’t very responsive.
According to Apple’s documentation, the best thing to do is to display a screen that looks just like your app would, but without any data in it yet. That’s pretty easy to do. Just open up MasterController.m, and make the following change:
// Replace tableView:numberOfRowsInSection's return statement to the following:
return 0; //return _bugs.count;
Then run the project on your device, and you’ll see an empty table view after it loads. In XCode, click on the Organizer button in the toolbar (the far right), click on your device, go to the screenshots tab, and click “New Screenshot” (the bottom right) to get a screenshot:
Taking a screenshot with Xcode Organizer
Then click Save as Launch Image. Make sure the ScaryBugs workspace is selected, keep the name as Default, and click Next. It will save the image to your project and set it as the launch image for you – handy, eh?
You can see that it’s set the image as the launch image by going to your target’s summary tab:
Setting the launch image in the Xcode project settings
Then restore tableView:numberOfRowsInSection to the way it was and run the app again. You should see a default screen as it loads instead of a blank view, and the app should feel a lot more responsive!

Bonus: Handling Long-Running Operations

If you run the app on the Simulator, everything probably appears fine, but if you run it on your iPhone and go to tap a picture to change it, there is a LONG delay as the UIImagePicker initializes. After picking a picture, there is another long delay as the image is resized (especially if it’s large). This is a very bad thing, as it makes your application seem unresponsive to users.
The main rule to keep in mind is that you should never perform long-running operations on the main thread. We’re currently violating this rule in two places, which is why our app appears unresponsive.
What you should do instead is run long-running operations on a background thread. Ideally, the operation would be done in the background as the user continues to do other things. But if the work is required to occur before the user can continue (such as loading the image picker), at the very least you should display a loading indicator of some sort so the user understands that the app is working and not jsut broken.
So that’s what we’ll do here – run the long-running code on a background thread, and display a “loading” view on the foreground thread while we wait for the operation to complete.
The desire to display a loading view is a common problem for app developers, so a lot of people have created some activity indicator libraries that we can use to save ourselves some time doing it ourselves. I’ve tried a bunch of these, my current favorite is SVProgressHUD by Sam Vermette, so let’s try that. You can download a copy off the SVProgressHUD Github page.
Once you’ve downloaded SVProgressHUD, add SVProgressHUD.h and SVProgressHUD.m to your project under the “Views” group. You also have to perform two configuration steps:
  1. Add required library. Click your project in the Project Navigator and select your ScaryBugs target. Select the Build Phases tab and expand the Link Binary with Libraries section. Click the + button and add QuartzCore.framework.
Adding required QuartzCore.framework library in Xcode
  1. Compile without ARC support. SVProgressHUD is not currently ARC compatible, so we need to compile it without ARC support. To do this, expand the Compile Sources tab and double click the entry for SVProgressHUD.m. Enter -fno-objc-arc into the dialog box to compile it without ARC support.
    Update 7/15/12: SVProgressHUD now has ARC support so this step is no longer necessary. Thanks Juan for pointing this out!
At this point you should be able to build your project without errors.
Then make the following changes to DetailViewController.m:
// At top of file
#import "SVProgressHUD.h"
 
// Replace addPictureTapped with the following:
- (IBAction)addPictureTapped:(id)sender {
if (self.picker == nil) {
 
// 1) Show status
[SVProgressHUD showWithStatus:@"Loading picker..."];
 
// 2) Get a concurrent queue form the system
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
// 3) Load picker in background
dispatch_async(concurrentQueue, ^{
 
self.picker = [[UIImagePickerController alloc] init];
self.picker.delegate = self;
self.picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
self.picker.allowsEditing = NO;
 
// 4) Present picker in main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController presentModalViewController:_picker animated:YES];
[SVProgressHUD dismiss];
});
 
});
 
} else {
[self.navigationController presentModalViewController:_picker animated:YES];
}
}
There’s a lot of new ideas here, so let’s go over this section by section.
  1. Here we use the SVProgressHUD helper class we just added to show a “Loading” GUI with a spinner on the screen. This way the user knows some work is going on and that the app hasn’t just locked up.
  2. We want to load the image picker in the background. You can do this on iOS with a technology called Grand Central Dispatch. We won’t go over it in huge detail here (but if you are curious, read this tutorial). For now, all you need to know is this line gives you a queue that you can use to run blocks of code in the background.
  3. This line executes a block of code to load the image picker in the background. If you are confused about the syntax of blocks, check out this tutorial.
  4. Finally, we present the picker on the main queue. Note you must always update the GUI on the main queue – you can’t do it on a background thread.
Similarly, we can perform the image resizing in the background as well. Replace imagePickerController:didFinishPickingMediaWithInfo with this:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {    
 
[self dismissModalViewControllerAnimated:YES];
 
UIImage *fullImage = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];
 
// 1) Show status
[SVProgressHUD showWithStatus:@"Resizing image..."];
 
// 2) Get a concurrent queue form the system
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
// 3) Resize image in background
dispatch_async(concurrentQueue, ^{
 
UIImage *thumbImage = [fullImage imageByScalingAndCroppingForSize:CGSizeMake(44, 44)];
 
// 4) Present image in main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.detailItem.fullImage = fullImage;
self.detailItem.thumbImage = thumbImage;
self.imageView.image = fullImage;
[SVProgressHUD dismiss];
});
 
});
 
}
And that’s it! Give it a run on your device, and you’ll see a new animation while the long-running work takes place, which makes for a much nicer user experience.
Using SVProgressHUD

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in this tutorial series.
Congratulations – you have finished creating a simple Master/Detail app for iOS! You have dived through a crash course of putting an app together, and can dig into further areas of interest.
Here’s what I’d recommend you read next:
I wish you best of luck in your iOS adventures, and I hope you enjoyed making this ScaryBugs app! If you have any questions or comments, please join the form discussion below!

No comments:

Post a Comment