Ad

Our DNA is written in Swift
Jump

UIImageView + Touch Handling = UIButton

gdscei asks:

“Hello, how can i make an image view work as a button? I want it to be assigned to the ‘reload’ action of WebView.
Can someone give me instructions how to do this?”

Often people start out constructing their UI visually, starting with this line of reasoning “I want to show an image. Ah, UIImageView”. So they create the visual appearance of their UI either by clicking it together in Interface Builder or – if slightly more advanced in their coding skills – creating those image views in code.

So if we create a new view-based project in XCode, we could modify the viewDidLoad of the main view controller like this to show the image:

- (void)viewDidLoad {
    [super viewDidLoad];
 
	UIImageView *imageView = [[UIImageView alloc] initWithFrame:
		CGRectMake(100.0, 100.0, 57.0, 57.0)];
	imageView.image = [UIImage imageNamed:@"Icon.png"];
	[self.view addSubview:imageView];
	[imageView release];
}

The next logical step in reasoning right after displaying the icon is now to get touch handling somehow. And that’s where you will get stuck because UIImageView is a dead end when it comes to reacting to the user’s finger.

Sure, if you consider yourself extra smart and that you won’t be stumped by such an inconvenient “cul de sac” then you will proceed to subclass UIImageView and adding touch handling via the touchesBegan, touchesMoved, touchesCancelled and touchesEnded method. And don’t go telling me that you would never make such a mistake. I know, I did and I’ve seen smarter programmers than the both of us proceed to painstakingly code their own touch handling for UIImageViews.

This post is NOT about doing that. I cannot prevent you from scratching your left ear with your right index finger, but I CAN show a smarter way to get your own interactive custom buttons.

I already gave away half of the solution: CUSTOM BUTTONS. Apple made it confusing to beginners by having the default style for a button being a rounded rectangle with text inside. That’s why there is such a strong trend to using UIImageViews because beginners cannot imagine that their beautifully crafted button images would fit into this rounded container. But in fact the rounded rectangle style is only one of several possibilities.

UIButtonType can have these values:

  • UIButtonTypeCustom,
  • UIButtonTypeRoundedRect
  • UIButtonTypeDetailDisclosure
  • UIButtonTypeInfoLight
  • UIButtonTypeInfoDark
  • UIButtonTypeContactAdd

The very first on UIButtonTypeCustom will be our choice for making our own custom button. For all the other button types Apple provides the drawing, for the custom one you have to bring your own, preferably in the form of UIImages for the various states.

Let’s have a brief look at the inheritance of UIImageView versus UIButton. This kinship was not obvious to me, I only discovered it by accident when I also was in the situation that I wanted to touch-enable some subviews.

UIImageView inherits from UIView : UIResponder : NSObject
UIButton inherits from UIControl : UIView : UIResponder : NSObject

So we can see that up to UIView both have the same set of methods and behaviors. An image view is just a UIView where an image from a property is being drawn in the view’s area, probably inside the drawRect. A button gains an additional level of inheritance: UIControl. If you read up on UIControl in the documentation it tells you that UIControl adds mechanisms to “convey user intent to the application”. It adds the target-action methodology to UIView where you can add targets and methods as actions that get executed when certain things happen. Like for example a touch is lifted inside a button or the value of a slider has changed.

But UIImageView and UIButton are views! Both have the genes of UIView inside of them and cannot deny their pedigree.

Knowing the above mentioned facts, why would you want to painstakingly code your own touch handling routines if you get them for free when choosing UIButton to display your icon image instead?

We can replace the above mentioned code with this:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    UIButton *imageButton = [UIButton buttonWithType:UIButtonTypeCustom];
    imageButton.frame = CGRectMake(100.0, 100.0, 57.0, 57.0);
    [imageButton setImage:[UIImage imageNamed:@"Icon.png"] forState:UIControlStateNormal];
    [self.view addSubview:imageButton];
}

Note that for buttons you generally don’t do alloc-initWithFrame, but use the factory method buttonWithType. Since this gives us an autoreleased button we don’t need to manually release it after addSubview.

If you do that you get exactly the same result, with a minor difference. If you tap the icon it darkens automatically to show the touch. That’s because UIButton expects to display something different when the button is touched. You can set a specific image for the highlighted state, if you don’t then UIButton takes the normal state image and darkens it automatically.

To disable this behavior change the property or set an image for the highlighted state:

imageButton.adjustsImageWhenHighlighted = NO;
// or
[imageButton setImage:[UIImage imageNamed:@"Icon_highlighted.png"]
	forState:UIControlStateHighlighted];

Possible control states are:

enum {
   UIControlStateNormal               = 0,
   UIControlStateHighlighted          = 1 << 0,
   UIControlStateDisabled             = 1 << 1,
   UIControlStateSelected             = 1 << 2,
   UIControlStateApplication          = 0x00FF0000,
   UIControlStateReserved             = 0xFF000000
};

Therefore control state is more than a single value, it actually can also be a combination of several bits. The normal state is 0, the highlighted state is 1, disabled 2 and selected 4. You can specify any combination thereof by using the bitwise or operator |. For example to set an image for selected OR highlighted you use UIControlStateSelected|UIControlStateHighlighted. This possibility is not clear if you look at the inspector for a button in Interface Builder. That’s why I prefer to do my buttons in code.

To clarify the states: A button at rest is “normal”. As long as you put your finger down on it, it’s “highlighted”. If you lift again, it returns to “normal”. If you set it’s enabled property to NO, then it’s “disabled”. If you set it’s selected property to YES, then it’s “selected”.

Cocoa Cracks NOTE that UIControlStateApplication actually shows us a range (16 bits) that we can use ourselves to define extra special states for our own controls. And UIControlStateReserved is a range (16 bits) that is reserved for Apple-internal use.

Ok, so much for states and looks. Adding targets and actions for the various possible interactions with a button is easy again:

[imageButton addTarget:self action:@selector(buttonPushed:)
	  forControlEvents:UIControlEventTouchUpInside];

We also have to implement the “action” which is just a method. The target is the class instance where you implement the method, the action is a selector, i.e. the “fingerprint” of the method.

- (void) buttonPushed:(id)sender
{
	NSLog(@"It works!");
}

Note that a pointer to your button instance will be passed via the sender parameter. This can sometimes be useful, for example if you want to use a single method to respond to multiple buttons. In this case you would use the tag of the passed sender to discern which button the call was coming from.

This method of making images respond to touches works with very little extra tying. I’m using it in DTCalendarView for the individual days as well as for a full screen tap recognition in DTSplashExtender. Best of all: it saves you lot of extra code or even having to subclass UIImageView.


Categories: Q&A

11 Comments »

  1. You’re article is very clear, but I can’t find the better way to realize this kind of “component” I need for my project:

    – it has to show an image
    – it has to show another image when touched
    – I need to set which image of two programmatically at any time
    – when touched, the component has to fire an event

    UIButton seems to be perfect, but I really can’t see how to change its state to change its image programmatically, as “state” is read-only.

    UIImageView could fit but I’d have the problems you mentioned trying to catch the user touches.

    What would you do in this case?

    tnx
    d

  2. I would create a UIControl which has a child view that is a UIButton and add properties for both images. Then I would set the first image for the “normal” state and the second image for the “selected” state. Changing the properties should update these settings. The touchUpInside event would trigger a method in the UIControl to toggle the selected property. I would probably have a BOOL property isON that is the same as the “selected” property of the UIButton. Then whenever this changes you send the control actions for ValueChanged.

  3. thanks for the fast answer. In the while I looked at the link posted by mattymee and it’s similar to your proposal, I think I’ll mix both methods.

    Thank you!

    d

  4. This is a REALLY well done tutorial. Helped me out a lot. I had actually gone down the path of drawing an image inside a view and got stuck on the button behavior. Your post saved the day and made it easy for me to get my images acting like buttons. Thanks a lot!

    scottyman

    Follow my joys and struggles learning iPhone app development at:

    http://www.myappventure.com

  5. I would like to have a progress bar inside my image/button.
    With an UIImageView, I can put a UIProgressView over the image, but my image cannot trigger any action as a button would do.
    With a UIButton, I can trigger an action, but I can’t put the UIProgressView over the button.

  6. yes you can.

  7. Thank you, I believed it was not possible.
    But, what can I do πŸ™‚ ? Add an action to a image, put a progress over a button, or both ?

    And moreover, do you have any clue on how do I can achieve this.

    Thank you

  8. Just letting you know that your article was very useful, thanks so much!

  9. You are the man! I had painted myself into a corner with a bunch of UIImageViews on a UIScrollView and I wanted them clickable. I was afraid I was gonna have to gut my whole app but after reading you I switched everything to UIButtons in like 15 minutes. So clearly and straightforwardly written. Will be back for more of your tutorials!

Trackbacks

  1. Making a Control @ Dr. Touch