Thursday, 13 October 2011

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



Perhaps the Scariest Bug of All!
Perhaps the Scariest Bug of All!
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 EditBugViewController and are using a UITableViewController for the RootViewController, most of the infrastructure is already in place! There are just three changes we have to make to RootViewController.m, but I’m going to explain them bit-by-bit to keep things easy to understand:
1) Set up navigation bar buttons
// Inside viewDidLoad
self.navigationItem.leftBarButtonItem = self.editButtonItem;
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] 
    initWithBarButtonSystemItem:UIBarButtonSystemItemAdd 
    target:self action:@selector(addTapped:)] autorelease];
First, in viewDidLoad, 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.
2) Implement tableView:commitEditingStyle:forRowAtIndexPath
// 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
// Add new method
- (void)addTapped:(id)sender {
    ScaryBugDoc *newDoc = [[[ScaryBugDoc alloc] initWithTitle:@"New Bug" rating:0 thumbImage:nil fullImage:nil] autorelease];
    [_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 tableView:self.tableView didSelectRowAtIndexPath:indexPath];
}
When the user taps the add button, 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.
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 ExtraStuffForScaryBugs.zip. Let’s set that as the icon for our project!
To do that, simply open up ScaryBugs-Info.plist, and modify the Icon file to read “logo1.png”:
Setting Icon in Info.plist
As you continue in your iOS development adventures, you’ll be coming back to Info.plist quite a bit to configure your projects in various ways.
There’s one other thing we should fix as well. If you try running ScaryBugs on your iPhone, 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 RootViewController.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, go to Window\Organizer, click on your device, go to the screenshots tab, and click “Capture” to get a screenshot:
Taking a Screenshot with Organizer
You can find these screenshots in /Users/yourUserName/Library/Application Support/Developer/Shared/Xcode/Screenshots. Find the one you took, rename it to “Default.png”, drag it over into your Resources folder to add it to your project.
Then restore tableView:numberOfRowsInSection to the way it was, and run it on your device again, and if all works well you should see a default screen as it loads instead of a blank view!

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 DSActivityView by David Sinclair, so let’s try that. You can download a copy off their page, or just grab a copy here.
Once you’ve downloaded DSActivityView, add the files to your project under the “Views” group. Then make the following changes to EditBugViewController.h:
// Before @interface
@class DSActivityView;
 
// Inside @interface
DSActivityView *_activityView;
NSOperationQueue *_queue;
 
// After @interface
@property (retain) DSActivityView *activityView;
@property (retain) NSOperationQueue *queue;
Here we declare our DSActivityView and something we haven’t discussed yet called an NSOperationQueue (more on this later).
Next make the following changes to EditBugViewController.m:
// At top of file
#import "DSActivityView.h"
 
// After @implementation
@synthesize activityView = _activityView;
@synthesize queue = _queue;
 
// At end of viewDidLoad
self.queue = [[[NSOperationQueue alloc] init] autorelease];
 
// In viewDidUnload
self.queue = nil;
 
// In dealloc
[_queue release];
_queue = nil;
 
// Replace addPictureTapped with the following:
- (IBAction)addPictureTapped:(id)sender {
    if (_picker == nil) {   
        [DSBezelActivityView newActivityViewForView:self.navigationController.navigationBar.superview withLabel:@"Loading Image Picker..." width:160];        
        [_queue addOperationWithBlock: ^{
            self.picker = [[UIImagePickerController alloc] init];
            _picker.delegate = self;
            _picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
            _picker.allowsEditing = NO;
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                [DSBezelActivityView removeViewAnimated:YES];
                [self.navigationController presentModalViewController:_picker animated:YES];    
            }];
        }];
    } else {
        [self.navigationController presentModalViewController:_picker animated:YES];
    }    
}
 
// Replace imagePickerController:didFinishPickingMediaWithInfo with the following:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {    
 
    [self dismissModalViewControllerAnimated:YES];
 
    [DSBezelActivityView newActivityViewForView:self.navigationController.navigationBar.superview withLabel:@"Resizing Image..." width:160];   
    [_queue addOperationWithBlock: ^{
        UIImage *fullImage = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage]; 
        UIImage *thumbImage = [fullImage imageByScalingAndCroppingForSize:CGSizeMake(44, 44)];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            _bugDoc.fullImage = fullImage;
            _bugDoc.thumbImage = thumbImage;
            _imageView.image = fullImage;
            [DSBezelActivityView removeViewAnimated:YES];
        }];
    }]; 
}
The first thing we do here is create an NSOperationQueue in our viewDidLoad. Details on the NSOperationQueue could easily fill an entire tutorial, so for now just think of it as an object you can submit tasks to execute to, and it will run them on a background thread for you.
Next, you’ll see that we’ve rewritten our addPictureTapped method a good bit. The first thing we do is use DSActivityView to present a loading screen by calling the newActivityForView method. We pass the navigation bar’s superview so the popup covers the entire screen, including the navigation bar.
Then we call a method on the operation queue to schedule some work to run on the background called addOperationWithBlock. Note that this is a new API only available on iPhone OS 4.0 or later (so it wouldn’t work on the iPad right now). There are other ways you can do the same thing on older OS’s, but I think this is API is pretty cool and a nice way of doing things if you can use it, so I wanted to show it off here.
Anyway, addOperationWithBlock uses a new feature of iOS called blocks. If you haven’t read up on it yet, I’d recommend checking out Apple’s excellent short practical guide to blocks.
So, everything inside our curly braces after addOperationWithBlock will be run on a background thread so the animation of the “Loading” view can continue and the user can see that the app is working away. Here we do the long-running work of initializing the image picker. When wer’e done, we need to stop the activity indicator and present the view controller – but we need to do that on the main thread.
Why? Well the rule of thumb is any time you need to modify the UI, you need to do that on the main thread. And we can use a special built-in NSOperationQueue called the “mainQueue” to queue up another block to run on the main thread once the other work is complete.
We follow the same idea in imagePickerController:didFinishPickingMediaWithInfo.
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.
Loading Indicator with DSActivityView

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in this tutorial series.
Please let me know if anything in the above is confusing or if you’d like me to go into more detail about anything.

Guess what – this isn’t the end for these bugs! We extend this project even more in a follow-up article on how to save your application data that you might enjoy if you’ve gotten this far, so check it out!

0 comments:

Post a Comment

 

Copyright @ 2013 PakTechClub.