This is part three of an ongoing series. You may wish to read or review the previous sections on iOS Development Basics and iOS Apps with Tasty UI. As the series goes on, we’re going to develop an application called “Orny”. We didn’t really explain it last article, but in a nutshell it is a tool that…
- Allows Ornithologists (or would-be ornithologists) to look up bird species
- Enables Ornithologists to take and track photos of birds (and their locations)
- Enables the sharing of that data with the Ornithological community (and others)
We’re going to progressively enhance the app as we learn, starting from something very simple and eventually winding up with something that could be released on the app store. Note that I’m not a designer, and the app probably won’t look all that pretty. Them’s the breaks.
Setting Up
This time, we’re going to build on a previous project. You can get the source I’m using as a starting point for this tutorial from Github BuildMobile Project Orny. The source for the app as it exists at the end of this tutorial will be made available on a separate branch in due course. Download the archive, unzip it, and open it in Xcode.
Lies to Children
In our previous tutorial, we implemented a UITableView to show a list of birds. We now want to make this table interactive, so that clicking on the name of a bird will show a picture of it. Before we go too far though, we should neaten up one aspect of our previous code.
A UITableView is made up of UITableViewCells. The code as it stands simply creates a cell for each and every entry in the table. Constantly creating cells is a bit inefficient. Apple gives us a fairly easy way to re-use existing cells once they’ve scrolled off-screen (preventing more use of delicious, precious memory.)
In BirdListViewController.m
, find the method tableView:(UITableView*)table cellForRowAtIndexPath
and change it to look like the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *newCell;
if((newCell = [tableView dequeueReusableCellWithIdentifier:@"birdList"]) == nil) {
newCell = [[[UITableViewCell alloc] initWithStyle:
UITableViewCellStyleDefault reuseIdentifier:@"birdList"] autorelease];
}
NSDictionary *thisBird = [birds objectAtIndex:[indexPath row]];
UILabel *newCellLabel = [newCell textLabel];
[newCellLabel setText:[thisBird objectForKey:@"name"]];
return newCell;
}
The key here is the use of dequeueReusableCellWithIdentifier
to get any “spare” cells. If there’s a cell sitting off-screen, not being displayed, we get a handle to that and “edit” it to be the cell we want. If none are spare, that method returns nil, and we create a brand new cell.
For this to work, all cells in a given table must use the same identifier (hence the use of @"birdList"
in [[UITableViewCell alloc] initWithStyle]
). Bear this in mind for any future use of this technique. Much better!
Making Clickable Rows
We now want to make our table rows clickable. When clicked, we’ll show the user an image of the bird selected (with some attribution text explaining where the image came from).
To do this, we need to have our BirdListViewController
conform to a portion of the UITableViewDelegate
protocol. To that end, modify BirdListViewController.h
:
@interface BirdListViewController : UIViewController UITableViewDataSource, UITableViewDelegate {
}
We only want to implement one specific method on BirdListViewController
– tableView:didSelectRowAtIndexPath
. Add the following to BirdListViewController.m
:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
NSDictionary *thisBird = [birds objectAtIndex:[indexPath row]];
NSString *message = [NSString stringWithFormat:@"You clicked: %@", [thisBird objectForKey:@"name"]];
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"You clicked a bird!" message:message delegate:self cancelButtonTitle:@"Neat!"otherButtonTitles:nil] autorelease];
[alert dismissWithClickedButtonIndex:0 animated:TRUE];
[alert show];
}
We’ve got the method in place – now we need to wire our UITableView to use our controller as a delegate. Open BirdListViewController.xib
, and right-click the UITableView.
Drag-and-draw from the circle to the right of the word ‘delegate’ to the ‘File’s Owner’ box on the left.
Yippee-kai-ay: Build and run, and you should see an alert pop up when you click on a table cell.
Creating our Image Display View
We’re now going to add a view for displaying images of our bird species. When the user selects a row in the table, instead of showing an alert, we’ll transition to the new view and display the image.
We’ll use a UINavigationController to accomplish this, but first, let’s create the image display view. I’m going to gloss over the steps a bit here, because we’ve created new UIViewControllers and XIB files a number of times before. To begin with:
- Add a new UIViewController sub-class called
BirdListDetailViewController
, with an XIB file - Edit the XIB file, and drag a UIImageView into the view
When you drag in the image view and hold it there a moment, you should find it resizes to take over the entire space of the view. Neat!
IBOutlet, you be Inlet?
We now want to connect our BirdListDetailViewController
to the UIImageView in the XIB file. We need to create a ‘handle’ to the UIImageView in the controller, and attach the UIImageView to the controller in the XIB file.
We do this by creating a property on the controller called an IBOutlet
(Interface Builder Outlet).
Make BirdListDetailViewController.h
look like the following:
@interface BirdListDetailViewController : UIViewController {
IBOutlet UIImageView *imageView;
}
@property (nonatomic, assign) IBOutlet UIImageView *imageView;
@end
We’ve declared a property, so we need to @synthesize it – the top of BirdListViewController.m
will need something like this:
@implementation BirdListDetailViewController
@synthesize imageView;
Switch over to the XIB, and right-click your UIImageView. You should see the usual right-click menu; drag-and-draw a line from ‘New Referencing Outlet’ to the ‘File Owner’ icon on the left.
Your controller is now aware of the existence of the UIImageView in your View (the XIB file). You don’t need to do this for every element – just the ones you want to interact with at run-time.
Get Some Images
We need some images of our birds, if we’re to display them to the user. Lucky for you I’ve found some we can use:
At the time of writing, both of these images were released under a Creative Commons license. In the case of the image of the Rosella by Flickr user symonty, the image is licensed under the Creative Commons Attribution No Derivatives license (CC-BY-ND) – we can use it, as long as we acknowledge the author and don’t change the image.
In the case of the Magpie image by Flick user teknorat, the license is Creative Commons Attribution Share Alike (CC-BY-SA) – we can use the image as long as we attribute the author, and share any derivatives of his work we make under the same license.
In either case, this means that if you plan to release an application using these resources to the public, you will need to include information on or around the image as to its origin, per the terms of the licenses these content authors have chosen.
In either case, download copies of these images, and store them somewhere you’ll remember (usually your Downloads folder). Make sure you name them in a sane way (ie. rosella.jpg
and magpie.jpg
) and note the lower case! Right-click on your project, and select “Add Files to ‘Orny’”
Select the files, make sure ‘Copy items into destination group’s folder (if needed)’ is checked, and click ‘Add’. If they’re not already, drag-and-drop the images into ‘Supporting Files’. This is not an essential step, but it’s good to keep things organised. We’ll need these images in just a moment.
Back and Forth with UINavigationController
We’ll use a UINavigationController to push views onto a stack, display them to the user, and control transitions (via buttons controls) back to the main screen.
This means we need to rewire our MainWindow_iPhone.xib
and MainWindow_iPad.xib
. Both of these files contain a Window and a View, with the View set to be the RootViewController of the Window.
Go into each file, and delete the View object (you can select it on the left, then hit delete to remove it). Find the Navigation Controller in the object panel, and drag it into the XIB file.
Right click the Window object, and drag-and-draw a line from ‘RootViewController’ to the ‘Navigation Controller’ object on the left icon bar (or the Navigation Controller in the edit view.)
Finally, select the Properties view, and change the ‘Custom Class’ of the Navigation Controller to be BirdListViewContoller
.
Now, when your application starts, our BirdListViewController
’s view will be placed inside the UINavigationController by default.
Loading the Image
Earlier we wrote a function to handle the case where a user clicks on a table row. We want to re-write this function to instead initialise a BirdListDetailViewController, tell it which image to load, and pop it onto the UINavigationController. This will present it to the user, with a ‘back’ button to come back to our main screen.
However, we need to be particular about the order in which these operations happen, and how we handle them. The BirdListDetailViewController will be initialised first, but its view won’t be ‘awoken’ from its XIB (or, in older parlance, NIB) file until the UINavigationController pushes the view into the user’s sight.
This means that we can’t set the image in the UIImageView inside the BirdListDetailViewController directly – instead, we’ll pass the filename, and have the BirdListDetailViewController load it when its view comes to life. First, we’ll add a ‘filename’ property to BirdListDetailViewController.h
:
@interface BirdListDetailViewController : UIViewController {
IBOutlet UIImageView *imageView;
NSString *filename;
}
@property (nonatomic, assign) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) NSString *filename;
@end
Remember, again, that we need to @synthesize in BirdListViewController.m
:
@implementation BirdListDetailViewController
@synthesize imageView, filename;
...
Next, we want the BirdListViewController to load that file into the image view when it loads. Keeping in mind what we’ve accomplished above, make viewDidLoad
in BirdListViewController.m
like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
UIImage *image = [UIImage imageWithContentsOfFile:
[[NSBundle mainBundle] pathForResource:self.filename ofType:@"jpg"]];
[self.imageView setImage:image];
}
Pushing and Gliding and Sliding
It’s a tortured Theophilus Thistler reference. Look it up on YouTube.
When the user clicks on a row, we want to ‘push’ the view onto the UINavigationController that is now the root view of our application. By default, all views added to a UINavigationController have a property set on them called navigationController
.
Modify tableView:didSelectRowAtIndexPath
in BirdListViewController.m
to resemble to following:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
NSDictionary *thisBird = [birds objectAtIndex:[indexPath row]];
BirdListDetailViewController *detail = [[BirdListDetailViewController alloc] initWithNibName:@"BirdListDetailViewController" bundle:[NSBundle mainBundle]];
detail.filename = [thisBird objectForKey:@"image"];
[[self navigationController] pushViewController:detail animated:YES];
[detail release];
}
In summary, this function:
- Gets the appropriate Dictionary item for the selected row
- Instantiates the BirdListDetailViewController
- Tells the BirdListDetailViewController which filename we want to load, and
- Pops the BirdListDetailViewController onto the UINavigationController
Filenames
In an earlier tutorial, we set up a data structure consisting of arrays and dictionaries to hold information about the bird species we’re listing to the user. Unfortunately, the earlier tutorial included full filename in that data structure.
Because of the way we’re invoking pathForResource
in BirdListDetailViewController’s loadView
method, we don’t actually want this. So, edit loadBirdData
in BirdListViewController.m
thusly:
-(void)loadBirdData {
birds = [[NSMutableArray alloc] init];
// Add a Magpie to our bird array
[birds addObject:
[NSDictionary
dictionaryWithObjects:[NSArray arrayWithObjects:@"Magpie", @"magpie", @"Black and white and crafty all over!", nil]
forKeys:[NSArray arrayWithObjects:@"name", @"image", @"description", nil]
]
];
// And another!
[birds addObject:
[NSDictionary dictionaryWithObjects:
[NSArray arrayWithObjects:@"Rosella", @"rosella", @"A red and blue parrot", nil]
forKeys:[NSArray arrayWithObjects:@"name", @"image", @"description", nil]
]
];
}
If you hit run, you should now see images when you click on the names of birds.
Challenge Mode!
This week, I’ll leave you with a few challenges.
- How can you change the title of the root view controller from “Root View Controller”?
- How can you change the back button on the BirdListDetailViewController?
- How would you go about changing the project to work with different file formats?
- What are some methods we can use to add more bird species to our table, without having to specify them manually in loadBirdData?
Conclusion and Further Reading
We’ve made our app significantly more interactive, and users can now see images of some of the bird species we’re listing. Next time, we’ll expand the application further by pulling bird species data in from a CoreData database!