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

Ray Wenderlich

This post is also available in: Chinese (Simplified), French, Japanese, Spanish, Korean

Apparently this Lady Bug isn't very scary!

Apparently this Lady Bug isn't very scary!

Update 6/02/14: Fully updated for iOS 7 by Jorge Jordán.

This iOS tutorial is the second part of a three-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 iOS tutorial series, you created an app that contained a list of bugs in a table view.

In this second iOS tutorial, you’ll learn how to create a detail view so that you can view a larger picture of the bugs, rate them, and change their pictures!

In the third and final iOS tutorial of the series, you’ll learn how to add new bugs, add an icon and default image to your project, and handle long-running operations.

So time to make some bugs! After all, isn’t that what programming’s all about? :]

View Controllers, Oh My!

Now that you have a list of bugs, it would be nice to be able to tap on the bug to bring up a screen where you can edit the bug’s name or picture, and rate the bug.

Most of the time in iPhone apps, for every screen of the app you have a class that is the View Controller for that screen. Right now your RWTMasterViewController appears on startup, which contains a table view. You will make it so that when you tap a bug, it brings up the RWTDetailViewController, and shows some info about the bug.

When you first ran the template, this was actually working, but when you changed the objects displayed by the table view (your bugs instead of the XCode’s template NSDate objects), tapping rows no longer sends the correct object to the detail view. You’ll fix that soon.

Each View Controller can contain multiple views. In your table view controller, you just had a single view – the table view. However in your details view controller, you’re going to need a bunch of views – you’re going to need a view for the name of the bug, a view for the image, a view for the rating, and several others.

Download Some Stuff!

Speaking of which – you’re going to need a 5-star rating view in this details screen – but the iPhone doesn’t come with one by default. However, I recently wrote an iOS tutorial on How To Make a Custom UIView in iOS 5: A 5-Star Rating View, so you’ll just use the view from that iOS tutorial again here.

Don’t worry about going throught that iOS tutorial now (unless you feel like it) – instead you can just download the Extra Stuff for Scary Bugs package that I put together for this project.

Go ahead and download the file then:

  • Create a new group named Views in XCode, and drag RWTRateView.h/RWTRateView.m to that group, making sure Copy items into destination group’s folder (if needed) is checked. Also make sure the target ScaryBugs is checked. This is the 5-star rating view code from the iOS tutorial.
  • Repeat for RWTUIImageExtras, except drag them to a new group named Helpers. This is some helper code you’ll need to resize images a bit later on.
  • Repeat for the three shocked face images made by my lovely wife, except drag them to a new group named Art. These are the images you’ll be using for the stars in your rating view for some humorous flair :]
  • Repeat for the logo1.png, also drag that to the Art group. You’ll be setting that as the app icon later.

Laying Out Your Detail View Controller With the Storyboard Editor

Ok – now you’re finally ready to go! Open Main.storyboard, and if you scroll to the far right you’ll see the Detail View Controller that the template made for you by default, with a Detail view content goes here label inside:

The detail view controller in the storyboard editor

The Storyboard Editor provides a visual way to construct your user interface in XCode. You drag and drop UI elements onto your view, set up their properties the way you want, and you can even connect the elements to properties in your View Controller classes.

The easiest way to understand it is to try it out! First, click on the view controller and go to Editor\Canvas\Show Bounds Rectangles – this will make it easier to see how you’re laying out the controls on the screen.

Delete the label that says Detail view content goes here – you won’t be needing that!

Then in the panel to the right, on the bottom part, make sure the third tab is selected for the Object Library. Drag a UITextField, UIImageView, and a UIView onto the text screen and arrange them like the following (the text field is on top):

Laying out the interface in the storyboard editor

Then select the UITextField and in the sidebar top section, make sure the fourth tab (the Attributes Inspector) is selected, so you can change some properties.

Set the Font to Custom\Helvetica\Bold\Size 18.0, the text alignment to center, the Clear Button behavior to Appears While Editing, the Capitalization to Words like the following:

Setting text field attributes

Then, switch over to the Size Inspector by clicking on the fifth tab of the sidebar.

Note: If you can’t see the Size Inspector you will need to uncheck Use Autolayout located on the first tab of the sidebar. The Autolayout topic is widely covered in this amazing tutorial

Back to Size Inspector and set up the autosizing attributes like the following:

Autosizing Attributes

This will make it so that when your view gets rotated to landscape, the text field stretches across the screen to become wider.

Next set up your UIImageView. In the fourth tab (Attributes Inspector) set the Mode to Aspect Fit, and in the fifth tab (Size Inspector) set the autosizing attributes to the following:

Autosizing Attributes

This makes the Image View grow or shrink to fill the available space, while keeping its edges the same distance from the edge of the screen no matter what, and scale the image to fit the best it can within the space while maintaining the image’s aspect ratio.

For the UIView, go to the third tab (Identity Inspector) and set the Class identity to RWTRateView so your 5-Star Rating view shows up there. Then in the fifth tab (Size Inspector) set the autosizing attributes to the following:

Autosizing Attributes

This makes it so it stretches left to right, but always stays the same height.

So far, so good! Next you need to add a few more controls to the screen so that the user can tap the UIImageView area to change the picture.

There are a couple ways you can do this, but one easy way it to create an invisible button on top of the UIImageView, and set it up so that you get a callback when the button is tapped. You can also add a UILabel underneath the picture to say “Tap to Change Picture” if there is no picture set.

So drag a Button from the library, and resize it to be the same exact size and position as the UIImageView. To make it invisible, in the fourth tab (Attributes Inspector) change the type to Custom and set it’s default Title to blank. Then in the fifth tab (Size Inspector) set the autosizing attributes to the following:

Autosizing Attributes

Finally, drag a UILabel from the library, place it in the middle of the UIImageView, and double click to edit the text, changing it to “Tap To Change Image.” Then change the text alignment to center. Also, drag the UILabel up a few positions in the XIB so it’s behind the UIImageView (the list goes from bottom to top):

Setting the label to show up in the back

Then in the fifth tab (Size Inspector) set the autosizing attributes to the following:

Autosizing Attributes

Before you move on, you can double check that you’ve gotten all of the autosizing attributes right by selecting the Detail View Controller, and in the fourth tab (Attributes Inspector) changing the orientation from Portrait to Landscape:

Testing autosizing attributes and orientation changes work OK in Storyboard editor

If something isn’t right, don’t worry – just change it back to Portrait and double check the settings.

Phew! You’ve added all of the controls you need, so all you need to do now is hook everything up to their outlets in your class.

To do this, first bring up the Assistant Editor (second button under the “Editor” section in the top toolbar), and make sure it’s set to Automatic\RWTDetailViewController.h:

Bringing up the assistant editor

Then control-drag from the Text Field down into RWTDetailViewController.h, right before the @end. A popup will appear allowing you to hook the Text Field up to a property in your class. Name it titleField, and click Connect.

Connecting the title field to an outlet

Repeat this for the Image View (but connect it to an outlet named imageView) and the Rate View (but connect it to an outlet named rateView). Also add the below import to RWTDetailViewController.h:

#import "RWTRateView.h"

You also want to make it so when the button is tapped, a method gets called on your class. To do this, control-drag from the Button right before the @end, like you did when connecting the other views. However, this time select Action as the connection type, name it addPictureTapped, and click Connect.

Connecting a button to a method in the Storyboard editor

Notice how it by default selects the “Touch Up Inside” event for you. This is good because it means when the user’s finger moves up from the button (i.e. they tapped it), your method will be called.

You can connect other actions to callback methods too. For example, there’s an action on the text field when the text changes, and you want to get a callback when this happens.

To do this, control-drag from the Text Field right before the @end and also set it to Action. By default it sets the event to Editing Did End – change this to Editing Changed. Name the method titleFieldTextChanged, and click Connect.

The last thing you have to do is set your class as the delegate of the text field. Sometimes receiving callbacks on actions of a view isn’t enough – they might have other information to tell you about, and the text field is an example of this.

To do this, control-click on the Text Field, and drag a line from the little circle to the right of the delegate entry up to the Detail View Controller, and release.

Setting the delegate of a text field

At this point, your RWTDetailViewController.h should look like this:

#import <UIKit/UIKit.h>
#import "RWTRateView.h"
 
@interface RWTDetailViewController : UIViewController
 
@property (strong, nonatomic) id detailItem;
 
@property (strong, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UITextField *titleField;
@property (weak, nonatomic) IBOutlet RWTRateView *rateView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
- (IBAction)addPictureTapped:(id)sender;
- (IBAction)titleFieldTextChanged:(id)sender;
 
@end

You may notice some funky types above – IBOutlet and IBAction. These are “magic keywords” that the Storyboard Editor looks for, in order to allow you to associate controls that you add in interface builder to properties on your class. Basically, if you put an IBOutlet or IBAction next to a property/method, Interface Builder will detect it so you can hook it up later.

By creating these with the Storyboard Editor, it already connected the properties to the controls for you automatically, but you can see the connections by control-clicking on one of the Detail View Controller. These things are called outlets btw.

You need to make some small tweaks to this to mark your view controller as implementing some delegates, adding a property for an image picker, and modifying your detailItem to mark it as specifically being a RWTScaryBugDoc, because that’s the detail item you are going to be displaying. So modify RWTDetailViewController.h to the following:

#import <UIKit/UIKit.h>
#import "RWTRateView.h"
 
@class RWTScaryBugDoc;
 
@interface RWTDetailViewController : UIViewController <UITextFieldDelegate, RWTRateViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
 
@property (strong, nonatomic) RWTScaryBugDoc *detailItem;
@property (strong, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UITextField *titleField;
@property (weak, nonatomic) IBOutlet RWTRateView *rateView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (strong, nonatomic) UIImagePickerController *picker;
 
- (IBAction)addPictureTapped:(id)sender;
- (IBAction)titleFieldTextChanged:(id)sender;
 
@end

OK finally done setting up the layout and header file – onto the implementation!

Implementing Your Detail View

You’re going to make a bunch of changes to RWTDetailViewController.m. There’s a lot of code here, so you better go over it part by part.

1) Import headers

// At top of file
#import "RWTScaryBugDoc.h"
#import "RWTScaryBugData.h"
#import "RWTUIImageExtras.h"
 
// Add in synthesize section
@synthesize picker = _picker;

You should be a pro at this by this point!

2) Set up Rate View

// Replace configureView with the following
- (void)configureView
{
    // Update the user interface for the detail item.
    self.rateView.notSelectedImage = [UIImage imageNamed:@"shockedface2_empty.png"];
    self.rateView.halfSelectedImage = [UIImage imageNamed:@"shockedface2_half.png"];
    self.rateView.fullSelectedImage = [UIImage imageNamed:@"shockedface2_full.png"];
    self.rateView.editable = YES;
    self.rateView.maxRating = 5;
    self.rateView.delegate = self; 
}

In configureView (which is called from viewDidLoad), you set up the properties of your RWTRateView. For more details, check out the How To Make a Custom UIView in iOS 5: A 5-Star Rating View tutorial.

3) Enable autorotation

// Implement the method shouldAutorotateToInterfaceOrientation
- (BOOL)shouldAutorotateToInterfaceOrientation {
    return YES;
}

In shouldAutorotateToInterfaceOrientation, you return YES since you went to do all the work of setting up the autosizing attributes in Interface Builder! This will allow the user to rotate this view between orientations, and your controls will re-layout according to the autosizing attributes you set up.

4) Set up initial UI state

// Add to the end of configureView
if (self.detailItem) {
    self.titleField.text = self.detailItem.data.title;
    self.rateView.rating = self.detailItem.data.rating;    
    self.imageView.image = self.detailItem.fullImage;
}

Here you simply set up your GUI based on the bug that was selected.

5) Handle text view and rating view

- (IBAction)titleFieldTextChanged:(id)sender {
    self.detailItem.data.title = self.titleField.text;
}
 
#pragma mark UITextFieldDelegate
 
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    return YES;
}
 
#pragma mark RWTRateViewDelegate
 
- (void)rateView:(RWTRateView *)rateView ratingDidChange:(float)rating {
    self.detailItem.data.rating = rating;
}

You set up titleFieldValueChanged to be called whenever the user changes the value of the text field, so you update the model as well whenever it changes.

textFieldShouldReturn is called when the user hits the return key on the keyboard. You call resignFirstResponder to get the keyboard to disappear off the screen when that happens.

rateView:ratingIsChanged is called when the user chooses a new rating since you set yourself as the RWTRateView‘s delegate, so when that happens you update your model.

In case you were wondering, the #pragma marks are just special lines that XCode can read to set up separators in the editor’s function list for organization’s sake:

Using pragma mark to organize code in Xcode

5) Display image picker and process results

- (IBAction)addPictureTapped:(id)sender {
    if (self.picker == nil) {   
        self.picker = [[UIImagePickerController alloc] init];
        self.picker.delegate = self;
        self.picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        self.picker.allowsEditing = NO;    
    } 
    [self presentViewController:_picker animated:YES completion:nil];
}
 
#pragma mark UIImagePickerControllerDelegate
 
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
   [self dismissViewControllerAnimated:YES completion:nil];
}
 
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {    
 
   [self dismissViewControllerAnimated:YES completion:nil];
 
    UIImage *fullImage = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage]; 
    UIImage *thumbImage = [fullImage imageByScalingAndCroppingForSize:CGSizeMake(44, 44)];
    self.detailItem.fullImage = fullImage;
    self.detailItem.thumbImage = thumbImage;
    self.imageView.image = fullImage;
}

You set up addPictureTapped to be called whenever the user taps the invisible button above the UIImage, so here you create the UIImagePicker (if it doesn’t exist already), and set the photo source to photo library (you can choose other things such as camera as well). You set yourself as the delegate so you can get callbacks when the user finished picking the picture. Finally, you present the image picker as a modal view controller, which means it takes up the whole screen.

Finally, you implement the image picker callbacks for when the user picks an image or cancels. Either way, you dismiss the modal view controller. If the user did pick the image, you get the full image and also a thumbnail version (which you resize with the RWTUIImageExtras class that you added earlier), and update both the model and the view.

OMG – you’re probably sick of writing code now eh? Don’t worry – you’re almost done, just gotta hook this baby in!

Integrating Your Detail View

This should be pretty quick. First, open Main.storyboard, and select the table view cell in the Master View Controller. Control-drag from that cell over to the Detail View Controller, and a popup will appear asking if you want to connect them as a Push, Modal, or Custom. Select Push, and you should see an arrow connecting the view controllers:

Connecting the view controllers with a push segue

Now you just need to make it pass the correct bug onto the detail view controller when a row is selected. To do this, open RWTMasterViewController.m and make the following changes:

// Implement the method didMoveToParentViewController
-(void)didMoveToParentViewController:(UIViewController *)parent{
    [self.tableView reloadData];
}
 
// Modify the prepareForSegue method by
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    RWTDetailViewController *detailController =segue.destinationViewController;
    RWTScaryBugDoc *bug = [self.bugs objectAtIndex:self.tableView.indexPathForSelectedRow.row];
    detailController.detailItem = bug;
}

First, note that in didMoveToParentViewController, you reload the data in the table. This is because when the user is in the detail view, they might change the name of the bug or the picture, and you want the updated name/picture to show up when they come back to the table view. One easy way to do that is to reload the entire table, so you do that here.

Next, remember that you set things up in the Storyboard Editor that whenever a row is tapped, it will push the Detail View Controller onto the stack. When this happens, the prepareForSegue will be called, so you have a chance to give the detail view controller any information it needs. In this case, you simply pass the selected bug on to display.

Finally, you’re done! Go ahead and compile and run your project, and if all goes well you should now be able to bring up the bug detail view, change the names of the bugs, change their pictures, rate them, and even rotate to any orientation!

Detail View Controller Example

Where To Go From Here?

Here is a sample project with all of the code you’ve developed so far in this iOS 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.

In the final part of the series, you’ll learn how to add and delete bugs, add an icon and default image to your project, and correctly handle long-running operations!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

104 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 , 7 ]
  • Hi,
    After finishing coding for the part 2 i was facing the problem as others :

    [UIView setNotSelectedImage:]: unrecognized selector sent to instance 0x68832f0"

    Earlier there were two solutions poposed.
    1) Add RateView.m and UIImageExtras.m to buildphases->Compile sources
    2) Link rateview outlet in storyboard properly

    1) did not work for me, because files were already added to compile sources and yet i was getting the same error

    To solve i went to storyboard :
    ->Selected RateView on the Detail View Controller .
    -> Select "Show Identity Inspector" tab (left of "Show the attributes inspector")
    ->Custom class was UIView , change it to RateView.
    ->Save it.
    ->I had to restart xcode to see the effect of the change.

    Thanks
    mravikiran
  • @sadlerj - you can get back to the older autosizing by loading up the main storyboard, then In the far right panel clicking on the leftmost tab labeled 'File Inspector' then unchecking the box that says 'Use Autolayout'. If that isn't quite clear, look at: http://www.raywenderlich.com/20881/begi ... art-1-of-2
    khill
  • Wait... uhh... where is the property for the picker to go and as what type? Atm I get a ton of errors for the picker's @synthesize.
    DKL
  • The method viewWillAppear is no longer available in iOS6. How do you implement reload the table data in iOS6? Thanks, Duy.
    duyhtq
  • viewWillAppear is still around.

    Quote from Apple's docs:

    viewWillAppear:

    Notifies the view controller that its view is about to be added to a view hierarchy.
    - (void)viewWillAppear:(BOOL)animated
    Parameters

    animated - If YES, the view is being added to the window using an animation.
    Richard Caseyrcasey
  • Good tutorials. Thank you. Part -2, linking to detailView was confusing as the instructions assumed you understood xcode well. Good help. Cheers
    Bala
  • I don't see "Size Inspector" on xCode 5
    TomClancy
  • FYI:

    Using Xcode 5 and iOS 7, I've changed the following:

    [self.navigationController presentModalViewController:_picker animated:YES];

    to

    [self.navigationController presentViewController:_picker animated:YES completion:nil];

    and the corresponding dismissal code (in two places) to [self dismissViewControllerAnimated:YES completion:nil];

    Also, rather than following these instructions:

    // Add at end of viewWillAppear
    [self.tableView reloadData];

    I instead created the following in MasterViewController.m to update the table view after leaving the detail view:

    -(void)didMoveToParentViewController:(UIViewController *)parent{
    [self.tableView reloadData];
    }

    (I also ignored any code to set the orientation as I couldn't find those in the files.)

    Great tutorials Ray! Thanks so much!
    Signorini
  • Everything was fine until the 5th tab / Inspector and having to set the autosizing for the UIImage and the rest. Autosizing was nowhere to be found! Same with the "example". Bummer. I'm unable to continue for now. May have to come back to this later.
    Thanks!
    rdority
  • Thanks for the tutorial. Something doesn't quite seem straightforward vis-a-vis the sizing of the elements such as the ImageView, etc. I see "autosizing" in your screen captures but despite my efforts to follow things carefully, when I select the View Controller and change to "landscape", things just don't adjust correctly. So I won't bother setting up all the "plumbing" ie connecting to the programming for now.

    I know a lot of folks will just berate me for saying this, but I think Apple could and should have done a better job of making it TOTALLY EASY to make GREAT apps for the Mac and iOS. Clearly it is possible, but how many needless years wasted??

    I thought VB6 was pretty good but this is like a huge step backwards!!! : (
    rdority
  • Thanks for amazing tutorial! I had a runtime error when I press scary face. :o
    libc++abi.dylib: terminating with uncaught exception of type NSException
    .Any help :?:
    xnorax
  • Second tutorial I follow of yours and a second time where things don't work. I'm stuck scratching my head. I'm a systems engineer so I'm not stupid. I followed the directions step by step. I resolved my last problem by re-compiling but this time certain parts of the program just isn't working. You make assumptions that we're pros not really explaining where things should go. I would argue that on a second tutorial one doesn't become a pro. I'm going to find a different tutorial site. I appreciate your hard work but these tutorials aren't working for me. I feel more confused and questions not being answered. I feel a tutorial should answer common questions that come up.

    Again my corrective criticism to your tutorials is you should have someone else do them so they can feel just as confused as us and then they can tell you how to write better tutorials.

    Maybe I'll try your video tutorials. These written beginner ones are just terrible.
    Pragmatous
  • Maybe my xcode is buggy. I had to reconnect the rateview outlet even though the code looked exactly the same. It's so bazaar.
    Pragmatous
  • Hi, to say I am new to Objective C is under statement. Currently do php and wanting to move over to IOS. I think Tutorials are great. Just have a stupid newby question. When I completed part 2 working fine to move to detail view with no errors, but the title and ratings do not show. Any suggestions of where to start looking? Thanks :lol:
    LouisWe
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Our Books

Our Team

Tutorial Team

  • Kyle Richter

... 50 total!

Update Team

  • Ray Fix

... 15 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Jiyeon Seo
  • Team Tyran
  • Heejun Han

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!