iPad for iPhone Developers 101 in iOS 6: Custom Input View Tutorial

This is the third part and final part of a three-part series to help get iPhone Developers up-to-speed with iPad development by focusing on three of the most useful classes: UISplitView, UIPopoverController, and Custom Input Views. By Ellen Shapiro.

Leave a rating/review
Save for later
Share

You’ll be making a Custom Input View that looks like this.

Update 3/7/2013: Fully updated for iOS 6 (original post by Ray Wenderlich, update by Ellen Shapiro).

This is the third part and final part of a three-part series to help get iPhone Developers up-to-speed with iPad development by focusing on three of the most useful classes: UISplitView, UIPopoverController, and Custom Input Views.

In the first part of the series, you 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, you added a popover view to allow changing the color of the label displaying the monster’s name.

In this part, you’re going to add a custom input view (or “keyboard”) to allow selecting a new weapon for each monster.

You’ll start out with where you left off the project after Part 2, so grab a copy if you don’t have it already.

Before You Get Started: A Quick Refactor

In this tutorial, you’ll need to use the Monster object’s Weapon property for a lot more than it was originally intended do. In fact, you’re going to be using it for so much that it makes more sense to break it out into its own separate object.

Never be afraid to refactor (ie, rewrite your code), especially when something that was originally much smaller mutates into something much larger – it can sometimes be a little bit of a pain up front, but it makes your code far easier to read, understand, and maintain.

To get started, go to File\New\File and choose the iOS\CocoaTouch\Objective-C class template. Name the class Weapon, make it a subclass of NSObject, click Next, and then Create.

First, you’re going to want to move a couple of things from the Monster files over to the Weapon files. You’ll move the enum that’s currently named Weapon over to Weapon.h and rename it to WeaponType in order to prevent compiler errors. You’ll also create an assign property to hold the Weapon object’s WeaponType.

Then you’ll move the weaponImage method declaration to Weapon.h and the implementation to Weapon.m, and update it to use the WeaponType property instead of the old Weapon property when it was part of Monster.h. Finally, you’ll add a simple factory method to create a new weapon.

To do all this, replace Weapon.h with this:

#import <Foundation/Foundation.h>

typedef enum {
    Blowgun = 0,
    NinjaStar,
    Fire,
    Sword,
    Smoke,
} WeaponType;

@interface Weapon : NSObject

@property (nonatomic, assign) WeaponType weaponType;

//Factory method to make a new weapon object with a particular type.
+(Weapon *)newWeaponOfType:(WeaponType)weaponType;

//Convenience instance method to get the UIImage representing the weapon.
-(UIImage *)weaponImage;

@end

Replace Weapon.m with this:

#import "Weapon.h"

@implementation Weapon

+(Weapon *)newWeaponOfType:(WeaponType)weaponType
{
    Weapon *weapon = [[Weapon alloc] init];
    weapon.weaponType = weaponType;
    
    return weapon;
}

-(UIImage *)weaponImage
{
    switch (self.weaponType) {
        case Blowgun:
            return [UIImage imageNamed:@"blowgun.png"];
            break;
        case Fire:
            return [UIImage imageNamed:@"fire.png"];
            break;
        case NinjaStar:
            return [UIImage imageNamed:@"ninjastar.png"];
            break;
        case Smoke:
            return [UIImage imageNamed:@"smoke.png"];
            break;
        case Sword:
            return [UIImage imageNamed:@"sword.png"];
        default:
            //Anything not named in the enum.
            return nil;
            break;
    }
}

@end

Now update Monster.h with this, to take out the methods you’ve now moved to the Weapon class:

#import <Foundation/Foundation.h>

@class Weapon;
@interface Monster : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *description;
@property (nonatomic, strong) NSString *iconName;
@property (nonatomic, strong) Weapon *weapon;

//Factory class method to create new monsters
+(Monster *)newMonsterWithName:(NSString *)name description:(NSString *)description 
    iconName:(NSString *)iconName weapon:(Weapon *)weapon;

@end

Update Monster.m as well, removing the implementations:


#import "Monster.h"
#import "Weapon.h"

@implementation Monster
 
+(Monster *)newMonsterWithName:(NSString *)name description:(NSString *)description 
    iconName:(NSString *)iconName weapon:(Weapon *)weapon
{
    Monster *monster = [[Monster alloc] init];
    monster.name = name;
    monster.description = description;
    monster.iconName = iconName;
    monster.weapon = weapon;
 
    return monster;
}
 
@end

Add this import to the top of LeftViewController.m:

#import "Weapon.h"

And then update the portion of initWithCoder: adding Monster objects to the array to the following so you can take advantage of the new Weapon object:

//Create monster objects then add them to the array.
[_monsters addObject:[Monster newMonsterWithName:@"Cat-Bot" description:@"MEE-OW"
    iconName:@"meetcatbot.png" weapon:[Weapon newWeaponOfType:Sword]]];
[_monsters addObject:[Monster newMonsterWithName:@"Dog-Bot" description:@"BOW-WOW" 
    iconName:@"meetdogbot.png" weapon:[Weapon newWeaponOfType:Blowgun]]];
[_monsters addObject:[Monster newMonsterWithName:@"Explode-Bot" description:@"Tick, tick, BOOM!" 
    iconName:@"meetexplodebot.png" weapon:[Weapon newWeaponOfType:Smoke]]];
[_monsters addObject:[Monster newMonsterWithName:@"Fire-Bot" description:@"Will Make You Steamed" 
    iconName:@"meetfirebot.png" weapon:[Weapon newWeaponOfType:NinjaStar]]];
[_monsters addObject:[Monster newMonsterWithName:@"Ice-Bot" description:@"Has A Chilling Effect" 
    iconName:@"meeticebot.png" weapon:[Weapon newWeaponOfType:Fire]]];
[_monsters addObject:[Monster newMonsterWithName:@"Mini-Tomato-Bot" description:@"Extremely Handsome" 
    iconName:@"meetminitomatobot.png" weapon:[Weapon newWeaponOfType:NinjaStar]]];

Finally, in RightViewController.m, add an import to the top of the file:

#import "Weapon.h"

And update how you’re setting the WeaponImageView’s image.

_weaponImageView.image = [_monster.weapon weaponImage];

Hit run – your application should look exactly as it did before, just with some differences under the hood that will make it easier to implement your Custom Input View for selecting a new weapon for your monster.

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 return a custom view for the “inputView” property.

However, if you’re using something else (like you are in this case with your 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 you have two more classes to write: a custom UIImageView, and your view controller for your custom input view. Let’s start with your custom input view controller.

Creating a Custom Input View Controller

Go to File\New\File… and choose the iOS\CocoaTouch\Objective-C class template. Name the class WeaponInputViewController and make it a subclass of UIViewController. Be sure to check both Targeted for iPad and With XIB for user interface. Since this view is not part of the navigation hierarchy, it’s going to be responsible for its own .xib instead of the Storyboard being responsible for it. Click Next and then Create.

Open WeaponInputViewController.xib. By default it makes the view the size of the iPad screen, but you want something much smaller in height. Under Simulated Metrics in the Attributes Inspector (4th tab), set the Size to “None” to allow you to resize the view, and set the Status Bar to “None” to remove the simulated status bar.

Now, either by dragging the view to resize or by using the Size Inspector (5th tab), set the main view’s width to 768 and height to 110.

Create 5 70×70 buttons with their titles removed along the left side of the view as follows, and a “Close” button at the end:

Creating the Custom Input View, Part 1

For auto-layout purposes, you should pin the leftmost button to the top and left of the superview, then pin the top alignments of each subsequent large button so they’re all the same distance from the top of the superview. You should also make sure to pin the widths and heights of each of the square buttons. Then, pin the vertical centers of the large buttons to the vertical center of the close button, so that the close button is in the middle of those other buttons.

Once you’re done with AutoLayout, change the background color of the main view to Light Gray Color to make the background stand out a bit. Then change the type of each Button from “Rounded Rect” to “Custom” and set the image for each button to the image of one of the weapons, as shown:

Creating a Custom Input View, Part 2

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

#import <UIKit/UIKit.h>
#import "Weapon.h" //Provides access to the WeaponType enum.

@protocol WeaponInputDelegate <NSObject>
@required
-(void)selectedWeaponType:(WeaponType)weaponType;
-(void)closeTapped;
@end

@interface WeaponInputViewController : UIViewController

@property (nonatomic, weak) IBOutlet UIButton *blowgunButton;
@property (nonatomic, weak) IBOutlet UIButton *fireButton;
@property (nonatomic, weak) IBOutlet UIButton *ninjaStarButton;
@property (nonatomic, weak) IBOutlet UIButton *smokeButton;
@property (nonatomic, weak) IBOutlet UIButton *swordButton;
@property (nonatomic, weak) id<WeaponInputDelegate> delegate;

-(IBAction)weaponButtonTapped:(UIButton *)sender;
-(IBAction)closeTapped;
@end

This should all look familiar – you set up a protocol so you can notify a listener when a weapon type is selected (or the close button), setup IBOutlet properties for each of the buttons, an IBAction which will work with any of the weapon buttons (and which takes a sender parameter so it can tell which UIButton was tapped), and then an IBAction which only works with the close button.

So let’s hook up those action methods now. Go back to WeaponInputViewController.xib and control click on “File’s Owner” to open up the list of outlets and actions. First, drag from each of the IBOutlets for the weapon buttons to the actual buttons themselves to hook these outlets up.

Then, control-drag from each weapon button to “File’s Owner” and hook the button up to the weaponButtonTapped method. Control-drag from the Close button to the closeTapped method as well. When you’re done, you should have five buttons hooked up to weaponButtonTapped: and one to closeTapped like so:

IBAction hooked up to multiple UIButtons

Then wrap it up by implementing the IBActions you’ve declared in WeaponInputViewController.m with the following, just before @end:

#pragma mark - IBActions
-(IBAction)closeTapped
{
    //Notify the delegate if it exists.
    if (_delegate != nil) {
        [_delegate closeTapped];
    }
}

-(IBAction)weaponButtonTapped:(UIButton *)sender
{
    //Create a variable to hold the selected weapon type.
    WeaponType selectedWeaponType;
    
    //Set the selected weapon based on the button that was pressed.
    if (sender == _blowgunButton) {
        selectedWeaponType = Blowgun;
    } else if (sender == _fireButton) {
        selectedWeaponType = Fire;
    } else if (sender == _ninjaStarButton) {
        selectedWeaponType = NinjaStar;
    } else if (sender == _smokeButton) {
        selectedWeaponType = Smoke;
    } else if (sender == _swordButton) {
        selectedWeaponType = Sword;
    } else {
        NSLog(@"Oops! Unhandled button click.");
    }
    
    //Notify the delegate of the selection, if it exists.
    if (_delegate != nil) {
        [_delegate selectedWeaponType:selectedWeaponType];
    }
}

As you can see, nothing too exciting or fancy here. All you do is notify your delegate when the various buttons get tapped. This ViewController has no knowledge that its view is actually being used as a custom input controller – you have to tell it to be one.

Custom View and Custom Input Controller

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

While you’re at it, you’ll have to make a few other tweaks to the image view. By default, UIImageViews don’t accept input and don’t become responders to input events, so you’ll have to enable support for that.
So let’s get started.

Go to File\New\File… and choose the iOS\Cocoa Touch\Objective-C class template. Name the class WeaponSelectorImageView, make it a subclass of UIImageView, click Next and then Create.

Update WeaponSelectorImageView.h follows:

#import <UIKit/UIKit.h>
#import "WeaponInputViewController.h" //Has Input delegate as well.
@class Weapon;

@protocol WeaponSelectorDelegate <NSObject>
-(void)selectedWeapon:(Weapon *)weapon;
@end

@interface WeaponSelectorImageView : UIImageView <WeaponInputDelegate>

@property (nonatomic, strong) Weapon *weapon;
@property (nonatomic, strong) WeaponInputViewController *weaponInputController;
@property (nonatomic, strong) IBOutlet id<WeaponSelectorDelegate> delegate;

@end 

Let’s explain this a bit. First, you create a delegate so that you can notify a listener when the Weapon is changed. You’re going to be receiving updates about what WeaponType the user wants to use, but you need to be able to notify object of what Weapon object you want to use.

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

Switch over to WeaponSelectorImageView.m and add in the code below. You’re going to split it into sections so you can explain it part by part. Start by overriding the weapon property’s setter so that you can set the image appropriately based on the weapon choice:

#import "WeaponSelectorImageView.h"
#import "Weapon.h"

@implementation WeaponSelectorImageView

#pragma mark - Overridden setters
-(void)setWeapon:(Weapon *)weapon
{
    //First make sure you're actually changing the weapon
    if (_weapon != weapon) {
        _weapon = weapon;
        
        //Update your image to use the weapon's image.
        self.image = [_weapon weaponImage];
    }
}

This will work both when the .weapon is set from RightViewController.m’s refreshUI method and when the weapon is set from the delegate method you’ll implement in a minute.

Next, you override several superclass methods from UIResponder to allow for showing a custom input view:

#pragma mark - Superclass overrides
-(BOOL)canBecomeFirstResponder
{
    //Says that this view can show an input view.
    return YES;
}

-(UIView *)inputView
{
    //Make sure the weaponInputController exists, and if not, create it.
    if (_weaponInputController == nil) {
        _weaponInputController = [[WeaponInputViewController alloc] initWithNibName:nil bundle:nil];
        _weaponInputController.delegate = self;
    }
    
    //Return the WeaponInputController's view as the input view.
    return _weaponInputController.view;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //Show the input view as soon as the imageView is touched, 
	//if it is not already showing.
    if (![self isFirstResponder]) {
        [self becomeFirstResponder];
    }
}

The inputView method is the most important – this tells the system what view should be used as a custom input view when the view becomes the firstResponder. You’re basically saying, “Instead of showing a keyboard when I become a first responder, use this custom view I made.”

Most inputViews that are provided by Apple are just keyboards, but you can return any kind of view you want by overriding this method in any direct or indirect UIResponder subclasses. More details on this technique can be found in Apple’s developer documentation on Custom Views for Data Input.

Finally, implement the WeaponInputDelegate methods to handle Weapon Type selection and the close command:

#pragma mark - WeaponInputDelegate methods
-(void)closeTapped
{
    //Dismiss the input view.
    [self resignFirstResponder];
}

-(void)selectedWeaponType:(WeaponType)weaponType
{
    //Set the instance variable.
    [self setWeapon:[Weapon newWeaponOfType:weaponType]];
    
    //Dismiss the input view.
    [self resignFirstResponder];
    
    //Notify the delegate of the change if it exists.
    if (_delegate != nil) {
        [_delegate selectedWeapon:_weapon];
    }
}

@end

Note that whenever you call resignFirstResponder, this automatically dismisses the inputView the same way you dismiss the keyboard when a UITextView or UITextField resigns the first responder.

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

Integrating into the RightViewController

Open MainStoryboard_iPad.storyboard, and in the Right View Controller select the UIImageView currently named weaponImageView next to “Preferred way to kill”. Go to the Identity Inspector (3rd tab) in the inspector and change the class from UIImageView to “WeaponSelectorImageView”.

Also, in the Attributes Inspector (4th tab) make sure you check the box for “User Interaction Enabled” – if this box is not checked, touchesBegan:withEvent: will never be called and your custom input view will not show up.

Finally, control drag from the WeaponSelectorImageView to the RightViewController object, and set the RightViewController as the weapon selector’s delegate.

Now go to RightViewController.h so you can update a few items. First, add an import to the import section:

#import "WeaponSelectorImageView.h" //Also has WeaponSelectorDelegate protocol

Then update the class declaration to show that the class conforms to the WeaponSelectorDelegate protocol:

@interface RightViewController : UIViewController <MonsterSelectionDelegate, UISplitViewControllerDelegate, ColorPickerDelegate, WeaponSelectorDelegate>

Then update the class of the weaponImageView from UIImageView to your custom subclass, WeaponSelectorImageView:

@property (nonatomic, weak) IBOutlet WeaponSelectorImageView *weaponImageView;

In RightViewController.m, start by updating the refreshUI method:

-(void)refreshUI
{
    _nameLabel.text = _monster.name;
    _iconImageView.image = [UIImage imageNamed:_monster.iconName];
    _descriptionLabel.text = _monster.description;
    
    //Setting the weapon on your custom imageview will automatically set
    //the image.
    _weaponImageView.weapon = _monster.weapon;
}

And finally, add the WeaponSelectorDelegate method to update the Monster’s weapon:

#pragma mark - WeaponSelectorDelegate method
-(void)selectedWeapon:(Weapon *)weapon
{
    //Check to make sure the weapon is changing.
    if (_monster.weapon != weapon) {
        //Update the weapon
        _monster.weapon = weapon; 
    }
}

By adding this method, when you change the monster’s weapon, you update the object so that it remembers your choice for the life of the application.

Re-run your application, and if all works well, you should be able to tap the icon to bring up a custom input view:

Finished Custom Input View

Note that a custom input view will always want to be the same width as the keyboard. In fact, the Apple documentation warns: “Although the height of these views can be what you’d like, they should be the same width as the system keyboard.” In almost all cases, this is as wide as the entire screen rather than just the view controller launching the input view.

Remember to take this into account when using AutoLayout to build your custom input views – otherwise you might wind up with a portion of your input view that isn’t visible when the iPad is rotated in a certain direction.

Show Me the Code!

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

This concludes our iPad for iPhone developers series. We hope you learned a lot – and if you have any questions or comments, please join the forum discussion below!

Contributors

Over 300 content creators. Join our team.