17 May 2010

iPad for iPhone Developers 101: Custom Input View Tutorial

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

 

We'll create a custom keyboard like this.

We'll create a custom keyboard like this.

This is the third part of a three-part series to help get iPhone Developers up-to-speed with iPad development by focusing on three of the most interesting new classes (at least to me): UISplitView, UIPopoverController, and Custom Input Views.

In the first part of the series, we made an app with a split view that displays a list of monsters on the left side, details on the selected monster on the right side.

In the second part of the series, we added a popover view to allow changing of colors.

In this part, we’re going to add a custom input view (or “keyboard”) to allow selecting your favorite way to kill these monsters.

We’ll start out with where we left off the project last time, so grab a copy if you don’t have it already.

Custom Input View Overview

To display a custom input view, what you need to depends if you’re using a UITextView or UITextField, or something else.

If you’re using a UITextView or UITextField, you’re in luck. All you need to do is assign a custom view to the “inputView” property.

However, if you’re using something else (like we are in this case with our UIImageView), you’ll need to make a custom subclass of the view you’re using so that you can override the inputView getter and return your own custom view.

So we have two classes to write: a custom UIImageView, and our view controller for our custom input view. Let’s start with our custom input view.

Creating a Custom Input View

Go to “File\New File…”, choose “UIViewController subclass”, make sure “Targeted for iPad” and “With XIB for user interface” are checked but “UITableViewController subclass” is NOT checked, and click Next. Name the class “WeaponInputViewController”, and click Finish.

Open up WeaponInputViewController.xib. By default it makes the view the size of the iPad screen, but we want something much smaller in height. Interface Builder won’t let us resize the default view, so delete the default view and add a new one, with width 768 and height 110. Also don’t forget to control-drag from “File’s Owner” to View to reconnect the default view for the owner.

Update:: Jerry Beers from the comments section below pointed out that instead of deleting/re-adding, you can just turn off the simulated UI elements, and then you can resize at will. Thanks Jerry!

Create 5 70×70 buttons along the left side of the view as follows:

Custom Input View Layout

Then set tye type of each Button from “Rounded Rect” to “Custom” and set the image for each button to a weapon, as shown:

Custom Input View Layout With Button Images

Now let’s fill in the class definition. Replace WeaponInputViewController.h with the following:

#import <UIKit/UIKit.h>
#import "Monster.h"
 
@protocol WeaponInputControllerDelegate
- (void)weaponTapped:(Weapon)weapon;
- (void)doneTapped;
@end
 
@interface WeaponInputViewController : UIViewController {
    id<WeaponInputControllerDelegate> _delegate;
}
 
@property (nonatomic, assign) id<WeaponInputControllerDelegate> delegate;
 
- (IBAction)blowgunTapped:(id)sender;
- (IBAction)fireTapped:(id)sender;
- (IBAction)ninjastarTapped:(id)sender;
- (IBAction)smokeTapped:(id)sender;
- (IBAction)swordTapped:(id)sender;
- (IBAction)doneTapped:(id)sender;
 
@end

This should all look familiar – we just set up a protocol so we can notify a listener when a weapon is selected (or the done button), and set up a bunch of action methods.

So let’s hook up those action methods now. Go back to WeaponInputViewController.xib and control-drag from each button to “File’s Owner and hook the button up to the appropriate action method.

Then wrap it up by replacing WeaponInputViewController.m with the following:

#import "WeaponInputViewController.h"
 
@implementation WeaponInputViewController
@synthesize delegate = _delegate;
 
- (BOOL)shouldAutorotateToInterfaceOrientation:
    (UIInterfaceOrientation)interfaceOrientation {
    return YES;
}
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
 
- (void)viewDidUnload {
    [super viewDidUnload];
}
 
- (void)dealloc {
    self.delegate = nil;
    [super dealloc];
}
 
- (IBAction)blowgunTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate weaponTapped:Blowgun];
    }
}
 
- (IBAction)fireTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate weaponTapped:Fire];
    }
}
 
- (IBAction)ninjastarTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate weaponTapped:NinjaStar];
    }
}
 
- (IBAction)smokeTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate weaponTapped:Smoke];
    }
}
 
- (IBAction)swordTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate weaponTapped:Sword];
    }
}
 
- (IBAction)doneTapped:(id)sender {
    if (_delegate != nil) {
        [_delegate doneTapped];
    }
}
 
@end

As you can see, nothing too exciting or fancy here. All we do is notify our delegate when the various buttons get tapped. In fact, you could say that this view has no knowledge that it’s even being used as a custom input view controller!

Custom View and Custom Input Controller

We want it to work so that when you tap the image, it brings up the input controller we just made. So like we discussed above, we’ll have to make a subclass of UIImageView so we can return our input view to display.

While we’re at it, we’ll have to make a few oher tweaks to the image view. By default, UIImageViews don’t accept input and don’t become responders to input events, so we’ll have to enable support for that.

So let’s get started. Go to “File\New File…”, choose “Objective-C subclass”, make sure “Subclass of NSObject” is selected, and click Next. Name the class “WeaponSelector”, and click Finish.

#import <Foundation/Foundation.h>
#import "WeaponInputViewController.h"
 
@protocol WeaponSelectorDelegate
- (void)weaponChanged:(Weapon)weapon;
@end
 
@interface WeaponSelector : UIImageView <WeaponInputControllerDelegate> {
    WeaponInputViewController *_weaponInputView;
    Weapon _weapon;
    id<WeaponSelectorDelegate> _delegate;
}
 
@property (nonatomic, retain) WeaponInputViewController *weaponInputView;
@property (nonatomic, assign) Weapon weapon;
@property (nonatomic, assign) IBOutlet id<WeaponSelectorDelegate> delegate;
 
- (UIView *)inputView;
 
@end

Ok let’s explain this a bit. First, we create a delegate so that we can notify a listener when the weapon is changed. Note we could have possibly re-used the WeaponInputControllerDelegate for this, but it isn’t as clean of a design because it muddles the dependencies.

Next, we declare this class as a WeaponInputControllerDelegate since we want to know when the user selects a button in the input view. We store the current selected weapon, and a pointer to the delegate to notify when it changes. Note we mark the delegate as an IBOutlet so we can assign it from Interface Builder later.

Finally, we declare a prototype for the override for the getter for the inputView that this class will contain.

Switch over to WeaponSelector.m and add in the code below. We’re going to split it into sections so we can explain it part by part.

#import "WeaponSelector.h"
 
@implementation WeaponSelector
@synthesize weaponInputView = _weaponInputView;
@synthesize weapon = _weapon;
@synthesize delegate = _delegate;

We start out by adding our synthesize statement like normal.

- (void)setWeapon:(Weapon)weapon {
    _weapon = _weapon;
    switch (weapon) {
        case Blowgun:
            self.image = [UIImage imageNamed:@"blowgun.jpg"];
            break;
        case Fire:
            self.image = [UIImage imageNamed:@"fire.jpg"];
            break;
        case NinjaStar:
            self.image = [UIImage imageNamed:@"ninjastar.jpg"];
            break;
        case Smoke:
            self.image = [UIImage imageNamed:@"smoke.jpg"];
            break;
        case Sword:
            self.image = [UIImage imageNamed:@"sword.jpg"];
            break;            
        default:
            break;
    }
}

We then override the weapon property’s setter so that we can set the image appropriately based on the weapon choice.

- (UIView *)inputView {
    if (_weaponInputView == nil) {
        self.weaponInputView = [[[WeaponInputViewController alloc] 
            initWithNibName:@"WeaponInputViewController" 
            bundle:[NSBundle mainBundle]] autorelease];
        _weaponInputView.delegate = self;
    }
    return _weaponInputView.view;
}

Here’s the most important part – we override inputView to return our own custom view. We just allocate the view controller if we haven’t already, set ourselves as the delegate, and return the view.

- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self becomeFirstResponder];
}

We override the method to say that yes we CAN become the first responder to input events, and upon a touch we set ourselves as the first responder. This has the effect of bringing up our input view.

- (void)weaponTapped:(Weapon)weapon {
    self.weapon = weapon;
    [self resignFirstResponder];
    [_delegate weaponChanged:weapon];
}
 
- (void)doneTapped {
    [self resignFirstResponder];
}

Here we implement the WeaponInputViewControllerDelegate methods. In the case where a weapon is tapped, we set our weapon to the new weapon (through our property, hence calling our setter override and setting the image as well). We then resign as the first responder, which has the effect of removing our custom input view. Finally we notify our delegate that the weapon has changed so it can take appropriate action.

If done is tapped, we just call resignFirstResponder to remove the input view.

- (void) dealloc
{
    self.weaponInputView = nil;    
    [super dealloc];
}
 
@end

At the end we add our cleanup code as usual.

Phew! We’ve done a lot here. Don’t worry we’re almost done – all we have to do now is replace the plain vanilla input view with our custom view, and handle the weaponChanged callback!

Integrating into the Right View

Double click RightViewController.xib, and select the UIImage next to “Preferred way to kill”. Go to the fourth tab in the inspector and change the class from UIImageView to “WeaponSelector”. Also, in the first tab in the inspector check “User Interaction Enabled.”

Inspector Class Settings

Inspector User Interaction Enabled Setting

Finally, control drag from WeaponSelector to “File’s Owner”, and set it as the delegate.

Now make the following changes to RightViewController.h:

// In import section
#import "WeaponSelector.h"
 
// Modify class declaration
@interface RightViewController : UIViewController <MonsterSelectionDelegate, 
    UISplitViewControllerDelegate, ColorPickerDelegate, WeaponSelectorDelegate> {
 
// Modify type of weaponView variable
WeaponSelector *_weaponView;
 
// Modify type of weaponView property
@property (nonatomic, retain) IBOutlet WeaponSelector *weaponView;

And make the following changes to RightViewController.m:

// Modify refresh as the following
- (void)refresh {
    _nameLabel.text = _monster.name;
    _iconView.image = [UIImage imageNamed:_monster.iconName];
    _descrLabel.text = _monster.descr;
    _weaponView.weapon = _monster.preferredWayToKill;
}
 
// Add the following new method
- (void)weaponChanged:(Weapon)weapon {
    if (_popover != nil) {
        [_popover dismissPopoverAnimated:YES];
    }
    _monster.preferredWayToKill = weapon;
}

Compile and run, and if all works well, you shoudl be able to tap the icon to bring up a custom image view!

Custom Input View Screenshot

Show Me the Code!

Here’s a copy of all of the code we’ve developed so far.

That’s all for the iPad for iPhone Developers 101 series for now. However soon I will write an article about how to port iPhone apps to the iPad!


Category: iPad

Tags: , ,

43 Comments

  1. Holly Jones (1 comments) says:

    I don’t see a link to the code – I think I’ve got a mistake and I’d like to compare yours to mine.

    Thanks for a great series!

  2. Ray Wenderlich (874 comments) says:

    Oops! Thanks Holly. I fixed the link to the download, should work now!

  3. Charlie Fulton (1 comments) says:

    Hey Ray, this looks like another awesome set of tutorials, can’t wait to go through them! I have it on the agenda for this weekend.

  4. Jerry Beers (1 comments) says:

    Nice write-up, thanks. Just a couple of quick comments. I think IB won’t let you resize the view because it has the simulated user interface elements set by default. It might be easier to remove those and resize the view than to delete and remember to hook up the view outlet. Also, you don’t need to check if the delegate is nil before sending a message to it. A message to a nil object just gets ignored. Now if the delegate had optional methods, you would check that those were implemented before calling them, but that’s another matter.

    Thanks again.

  5. Eric (15 comments) says:

    Nice series Ray, thank you !

    Just one comment, you could have only one action method for WeaponInputViewController instead of one for each button. You just have to give them a title for the default state configuration in IB and have an action method something like that :

    -(void)buttonTapped:(id)sender
    {
    NSString *buttonTitle = [sender titleForState: UIControlStateNormal];

    if ([buttonTitle isEqualToString:@"blowgun"]) {
    [_delegate weaponTapped:Blowgun];
    } else if ([buttonTitle isEqualToString:@"ninjastar"]) {

    } else if …

    }

  6. Ray Wenderlich (874 comments) says:

    @Jerry: Awesome, I was always wondering why you couldn’t resize those default views!! You are right, turning off the simulated user interface elements does the trick, thanks!! I’ve updated the post noting your comment. Good point on the unnecessary nil check too!

    @Eric: Good point on that too! I think that may be a matter of style though, I didn’t see much of a difference between doing that and just having different methods for each, since in the end you need to have a different line of code for each case.

  7. Eric (15 comments) says:

    You’re right, maybe it’s only a matter of style.

    I got an idea to improve my function, using the tag property rather than to give a title, we can reuse the enum and just have to define a tag value kClose for the close button. Then you can write :
    -(IBAction)buttonTapped:(id)sender
    {
    if (sender.tag < kClose) {
    [_delegate weaponTapped:sender.tag];
    } else if (sender.tag == kClose) {
    [_delegate doneTapped];
    }
    }

    and in general case, you may use a switch statement to handle each button separately and you still have the possibility to group them to do the same thing once :
    -(IBAction)buttonTapped:(id)sender
    {
    switch(sender.tag) {
    case Blowgun:
    case NinjaStar:
    case Fire:
    [_delegate weaponTapped: sender.tag];
    break;

    case kClose:
    [_delegate doneTapped];
    break;
    }

    }

  8. Ray Wenderlich (874 comments) says:

    @Eric: Good point, using the tags to line up with the enums is a great idea!

  9. Avinash (2 comments) says:

    Hi Ray,
    Thanks a Lot. I’m a noob to iPhone OS and your tutorials helped me lot. I played a lot with Split Views and Custom input views. Thing that gave me a tough time is, how to add a navigation controller and control the flow of screens in the application. For e.g. SplitViewController (LeftViewController and RightViewController) should be replaced by another simple UIViewController screen using pushviewcontroller/popviewcontroller methods. Plz advice.

  10. Ray Wenderlich (874 comments) says:

    @Avinash: So which do you want as the root – the navigation controller (with the split view controller within), or the split view controller (with a navigation controller in each side)?

    Assuming you want the navigation controller as the root, it’s simply a matter of dragging a navigation controller into the MainWindow.xib and setting a split view controller as the root view controller within the navigation controller. What part are you getting stuck at?

    Also you may wish to check the HIG if you indeed want the navigation controller as the root to make sure that is all-OK, as I think it’s more common to have uisplitview as the root and navigation controllers within.

  11. Sean Batson (1 comments) says:

    How about dismissing the popover on tapping the bar button?

  12. Ray Wenderlich (874 comments) says:

    @Sean: Good point! I’ve updated the post & sample project with this. Thanks!

  13. Jason (19 comments) says:

    Thanks again Ray for another great set of tutorials. I’ve read a lot of books on iPhone development and find your blog posts to be some of the best resources out there. Keep up the good work!

    Also, been meaning to let you know that my Cocos2D project The Quiet Game (http://bit.ly/ailTTI) has been in the store for a while. Your Cocos2D tutorials were instrumental in getting the project going.

  14. Ray Wenderlich (874 comments) says:

    @Jason: Thanks for the kind words! And very cool, congrats on your release! I like your company’s web site as well, looks like a great place to work!

  15. Sean (8 comments) says:

    @Ray
    Can you do a tutorial on adding a textField to toolBar in code only no IB, and attach it to the keyboard subview of the ipad?

  16. Ray Wenderlich (874 comments) says:

    @Sean: Thanks for the idea! Generally I try to post on things that are more general interest than that though, so I’m not sure if I will get to that or not.

  17. Jonhy (2 comments) says:

    Hi Ray!

    Thanks for your tutorial, everything works fine.

    my question is how we can make custom dialog box to take input huge text from user

  18. Jonhy (2 comments) says:

    Hi Ray!

    Thanks for your tutorial.

    My Question is that i want to show a custom badge control on UIBarButtonItem .

    I know badge control is available for UITabbar .

    But i need for UIBarButtonItem on UIToolbar

    please help.

  19. Wikki (3 comments) says:

    Hi Ray .Thanks a Lot.

    Look this link at once.

    http://www.freeipadapps.net/wp-content/uploads/2010/04/box-net-screenshot-comments.jpg

    I want to make this type of comment bar.

    please arrange a new tutorial for that.

  20. Ray Wenderlich (874 comments) says:

    @Johny: I’m not sure how either of those questions are relevant to a Custom Input View…

    @Wikki: I doubt I’m going to write a tutorial like that, as that is something quite specific, and I tend to prefer writing general-use tutorials. You should be able to figure out how to write that with the knowledge of this tutorial combined with some general knowledge of iPhone UI programming.

  21. Ron (2 comments) says:

    Great tutorial, a cut above most out there. I’ll be sure to work through your others.

    I’m a software developer new to iPhone/iPad and I was looking for a method of overlaying/replacing the standard keyboards for specific numeric input. InputView will work splendly for me.

    Ron

  22. Wikki (3 comments) says:

    Hi

    Ray please give me the new tutorial link of

    http://www.freeipadapps.net/wp-content/uploads/2010/04/box-net-screenshot-comments.jpg

    this custom keyboard
    thanks

  23. Ray Wenderlich (874 comments) says:

    @Ron: Thanks much for the kind words, I’m glad you like them :] Yes, I think the custom input view is a great new feature!

  24. Mark (9 comments) says:

    Ray, thanks for this set of articles. They are a great help!
    I downloaded the project to try out some things, and have not been able to figure out how to get the colorPickerPopover to dismiss when you click elsewhere on the toolbar, the colorPicker button itself, or on the main button. Do you have any ideas on this?

    Thanks!
    Mark

  25. Mark (9 comments) says:

    I think I figured out part of it anyway – in setColorButtonTapped:(id)sender I put this check in after the if (_colorPicker == nil) check:

    if (self.colorPickerPopover.isPopoverVisible) {
    [self.colorPickerPopover dismissPopoverAnimated:YES];
    }

  26. Ray Wenderlich (874 comments) says:

    @Mark: Thanks, and glad you got it working!

  27. Tera (2 comments) says:

    Ray, thanks for your helpful tutorial.

    I found that
    when ‘shouldAutorotateToInterfaceOrientation’ returns YES, weapon input view dose not rotate correctly according to device rotation.

    When ‘shouldAutorotateToInterfaceOrientation’ returns NO,it works.

  28. Ray Wenderlich (874 comments) says:

    @Tera: I just downloaded the sample project and didn’t see any unexpected behavior with autorotation – but maybe I’m missing something, could you provide some more information?

  29. Tera (2 comments) says:

    Hi Ray.

    >could you provide some more information?

    I can reproduce the problem on my environment as below.

    0)Set device in portrait orientation.

    1)Touch weapon selector.
    => Weapon input view is shown, and arranged correctly.

    2)Rotate device(clockwise).
    => Weapon input view is go away from screen.

    It occurs with real iPad device and iPhone simulator.

    My developing environment is
    X-code 3.2.3 (1688)
    iPhone OS 3.2:(7B500)
    iPhone Simulator 3.2:(7W367a)

    When shouldAutorotateToInterfaceOrientation returns NO, it works correctly.
    ——————-
    WeaponInputViewController.m
    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return NO;
    // return YES;
    }
    ——————-

  30. Donnie (1 comments) says:

    Wow..this is a great tutorial. Just learning this stuff and my brain is hurting!
    Question I have…is it possible to add user input, so the user can add a new bot, pick an icon (etc) and add it to the root view controller?

  31. Ray Wenderlich (874 comments) says:

    @Tera: Thanks for the info! Wow… I just reproduced that over here too, very strange behavior eh? Good to know about that.

    @Donnie: Yeah you can definitely add user input to this if you want. In fact that’s basically the design of my app “Level Me Up”…

  32. indrajeet (1 comments) says:

    Good job Ray..//
    I am curious to know that is it possible to make split view application with more than two partitions and also how to make d customize inputs scroll..Thanks in advance

  33. Ray Wenderlich (874 comments) says:

    @indrajeet: You can’t customize UISplitView very much, so if you want to do stuff like that you might want to check out Matt Gemmell’s MGSplitViewController:

    http://mattgemmell.com/2010/07/31/mgsplitviewcontroller-for-ipad

  34. Nick (7 comments) says:

    As pointed out above, there is an issue with the rotation of the device and the WeaponInputViewController.

    In shouldAutorotateToInterfaceOrientation, you should return NO instead of YES. This fixes the rotation issue, but the weapon input view will overlap the table view when in Landscape mode.

    Thanks for the tutorial!

  35. iPadNewbie (1 comments) says:

    Thanks for the great tutorial.

    Is it not possible to have the custom input view (weapons chooser) appear so that it does not overlap the master or detail view? May be as a neat box at the bottom of the detail view? How would I be able to do it?

  36. Ray Wenderlich (874 comments) says:

    @iPadNewbie: AFAIK, I think if you wanted to do that you’d have to use standard UIViews and managing adding them to the view hierarchy yourself, rather than using custom input views.

  37. olinsha (16 comments) says:

    Hi Ray,
    sorry but I don’t understand what you mean by “instead of deleting/re-adding, you can just turn off the simulated UI elements, and then you can resize at will”
    May you explain how to turn off the simulated UI Elements ? Because until now I was doing like you : deleting then creating a new view to be able to resize it.

  38. Ray Wenderlich (874 comments) says:

    @olinsha: Ah yeah I didn’t know you could do this for a long time either till someone on this site pointed it out to me. The way you do it is select the root view in Interface Builder, and in the Attributes Inspector you’ll see the first section reads “Simulated UI elements”. Switch the selections for Status Bar, Top Bar, and Bottom Bar to read “Unspecified”, then you can resize the view at will!

  39. Richard (6 comments) says:

    Great Tutorial Ray!

    If anyone goes through the initial setup and finds that their table view doesn’t show up and their RightViewController takes up the whole screen like me the first few times, you must make absolutely sure you targeted the “iPad” by checking the “Target for iPad” checkbox for both your view controllers, this is critical. If this box is not checked, you be targeting iPhone, which won’t work. Also, the table will only show up in landscape orientation, so make sure you rotate the device/simulator.

    Cheers

  40. Ray Wenderlich (874 comments) says:

    @Richard: Thanks much for posting your solution here for everyone else! :]

  41. mikezang (1 comments) says:

    Hi,
    I want to make a Chinese custom input, it said multistage input, I want to know how can I show the multiple characters when I typed a letter on keyboard, do you have any idea?

  42. Ray Wenderlich (874 comments) says:

    @mikezang: When you make a custom input view you have complete control over what you put inside and how it works. So for example you could have a custom view, that has a gesture recognizer to detect taps, and changes its look when it’s pressed down.

I'd love to hear your thoughts!