Channels ▼
RSS

Mobile

Gestures and Touches in iOS6: More Recipes


Live Touch Feedback

Have you ever needed to record a demo for an iOS app? There's always compromise involved. Either you use an overhead camera and struggle with reflections and the user's hand blocking the screen, or you use a tool like Reflection but you only get to see what's directly on the iOS device screen. These app recordings lack any indication of the user's touch and visual focus.

Recipe 7 offers a simple set of classes (called TOUCHkit) that provide a live touch feedback layer for demonstration use. With it, you can see both the screen that you're recording as well as the touches that create the interactions you're trying to present. It provides a way to compile your app for both normal and demonstration deployment. You don't change your core application to use it; it's designed to work as a single toggle, providing builds for each use.

To demonstrate this, the code shown in Recipe 7 is bundled in the sample code repository with a standard Apple demo. This shows how you can roll the kit into nearly any standard application.

Enabling Touch Feedback

You add touch feedback by switching on the TOUCHkit feature, without otherwise affecting your normal code. To enable TOUCHkit, you set a single flag, then compile and use that build for demonstration, complete with touch overlay. For App Store deployment, you disable the flag. The application reverts to its normal behavior, and there are no App Store unsafe calls to worry about:

#define USES_TOUCHkit    1

This recipe assumes that you're using a standard application with a single primary window. When compiled in, the kit replaces that window with a custom class that captures and duplicates all touches, allowing your application to show the user's touch bubble feedback.

There is one key code-level change you must make, but it's a very small one. In your application delegate class, you define a WINDOW_CLASS to use when building your iOS screen:

#if USES_TOUCHkit
#import "TOUCHkitView.h"
#import "TOUCHOverlayWindow.h"
#define WINDOW_CLASS TOUCHOverlayWindow
#else
#define WINDOW_CLASS UIWindow
#endif

Then, instead of declaring a UIWindow, you use whatever class has been set by the toggle:

WINDOW_CLASS *window;
window = [[WINDOW_CLASS alloc] 
    initWithFrame:[[UIScreen mainScreen] bounds]];

From here, you can set the window's rootViewController as normal.

Intercepting and Forwarding Touch Events

The key to this overlay lies in intercepting touch events, creating a floating presentation above your normal interface, and then forwarding those events on to your application. A TOUCHkit view lies on top of your interface. The custom window class grabs user touch events and presents them as circles in the TOUCHkit view. It then forwards them as if the user were interacting with a normal UIWindow. To accomplish this, this recipe uses event forwarding.

Event forwarding is achieved by calling a secondary event handler. The TOUCHOverlayWindow class overrides UIWindow's sendEvent: method to force touch drawing, and then invokes its superclass implementation to return control to the normal responder chain.

The following implementation is drawn from Apple's Event Handling Guide for iOS. It collects all the touches associated with the current event, allowing multitouch as well as single touch interactions, dispatches them to TOUCHkit view layer, and then redirects them to the window via the normal UIWindow sendEvent: implementation:

@implementation TOUCHOverlayWindow
- (void) sendEvent:(UIEvent *)event
{
    // Collect touches
    NSSet *touches = [event allTouches];
    NSMutableSet *began = nil;
    NSMutableSet *moved = nil;
    NSMutableSet *ended = nil;
    NSMutableSet *cancelled = nil;
    
    // Sort the touches by phase for event dispatch
    for(UITouch *touch in touches) {
        switch ([touch phase]) {
            case UITouchPhaseBegan:
                if (!began) began = [NSMutableSet set];
                [began addObject:touch];
                break;
            case UITouchPhaseMoved:
                if (!moved) moved = [NSMutableSet set];
                [moved addObject:touch];
                break;
            case UITouchPhaseEnded:
                if (!ended) ended = [NSMutableSet set];
                [ended addObject:touch];
                break;
            case UITouchPhaseCancelled:
                if (!cancelled) cancelled = [NSMutableSet set];
                [cancelled addObject:touch];
                break;
            default:
                break;
        }
    }
    
    // Create pseudo-event dispatch
    if (began)     
        [[TOUCHkitView sharedInstance] 
            touchesBegan:began withEvent:event];
    if (moved)
        [[TOUCHkitView sharedInstance] 
            touchesMoved:moved withEvent:event];
    if (ended)     
        [[TOUCHkitView sharedInstance] 
            touchesEnded:ended withEvent:event];
    if (cancelled) 
        [[TOUCHkitView sharedInstance] 
            touchesCancelled:cancelled withEvent:event];
    
    // Call normal handler for default responder chain
    [super sendEvent: event];    
}
@end

Implementing the TOUCHkit Overlay View

The TOUCHkit overlay is a single clear UIView singleton. It's created the first time the application requests its shared instance, and the call adds it to the application's key window. The overlay's user interaction flag is disabled, allowing touches to continue on through the responder chain, even after processing those touches through the standard began/moved/ended/cancelled event callbacks.

The touch processing events draw a circle at each touch point, creating a strong pointer to the touches until that drawing is complete. Recipe 7 details the callback and drawing methods that handle that functionality.

Recipe 7: Creating a Touch Feedback Overlay View.
+ (id) sharedInstance 
{
    // Create shared instance if it does not yet exist
    if(!sharedInstance)
    {
        sharedInstance = [[self alloc] initWithFrame:CGRectZero];
    }
    
    // Parent it to the key window
    if (!sharedInstance.superview)
    {
        UIWindow *keyWindow= [UIApplication sharedApplication].keyWindow;
        sharedInstance.frame = keyWindow.bounds;
        [keyWindow addSubview:sharedInstance];
    }
    
    return sharedInstance;
}

// You can override the default touchColor if you want
- (id) initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        self.backgroundColor = [UIColor clearColor];
        self.userInteractionEnabled = NO;
        self.multipleTouchEnabled = YES;
        touchColor = 
            [[UIColor whiteColor] colorWithAlphaComponent:0.5f];
        touches = nil;
    }
    
    return self;
}

// Basic Touches processing
- (void) touchesBegan:(NSSet *)theTouches withEvent:(UIEvent *)event
{
    touches = theTouches;
    [self setNeedsDisplay];
}

- (void) touchesMoved:(NSSet *)theTouches withEvent:(UIEvent *)event
{
    touches = theTouches;
    [self setNeedsDisplay];
}

- (void) touchesEnded:(NSSet *)theTouches withEvent:(UIEvent *)event
{
    touches = nil;
    [self setNeedsDisplay];
}

// Draw touches interactively
- (void) drawRect: (CGRect) rect
{
    // Clear
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextClearRect(context, self.bounds);
    
    // Fill see-through
    [[UIColor clearColor] set];
    CGContextFillRect(context, self.bounds);
    
    float size = 25.0f; // based on 44.0f standard touch point
    
    for (UITouch *touch in touches)
    {
        // Create a backing frame
        [[[UIColor darkGrayColor] colorWithAlphaComponent:0.5f] set];
        CGPoint aPoint = [touch locationInView:self];
        CGContextAddEllipseInRect(context, 
            CGRectMake(aPoint.x - size, aPoint.y - size, 2 * size, 2 * size));
        CGContextFillPath(context);
        
        // Draw the foreground touch
        float dsize = 1.0f;
        [touchColor set];
        aPoint = [touch locationInView:self];
        CGContextAddEllipseInRect(context, 
            CGRectMake(aPoint.x - size - dsize, aPoint.y - size - dsize, 
                2 * (size - dsize), 2 * (size - dsize)));
        CGContextFillPath(context);
    }

    // Reset touches after use
    touches = nil;
}

Adding Menus to Views

The UIMenuController class allows you to add pop-up menus to any item that acts as a first responder. Normally, menus are used with text views and text fields, enabling users to select, copy, and paste. Menus also provide a way to add actions to interactive elements like the small drag views used throughout this article. Figure 4 shows a customized menu. In Recipe 8, this menu is presented after long-tapping a flower. The actions will zoom, rotate, or hide the associated drag view.

This recipe demonstrates how to retrieve the shared menu controller and assign items to it. Set the menu's target rectangle (typically, the bounds of the view that presents it), adjust the menu's arrow direction, and update the menu with your changes. The menu can now be set as visible.

Menu items work with standard target-action callbacks, but you do not assign the target directly. Their target is always the first responder view. This recipe omits a canPerformAction:withSender: responder check, but you'll want to add that if some views support certain actions and other views do not. With menus, that support is often tied to state. For example, you don't want to offer a copy command if the view has no content to copy.


Figure 4: Contextual pop-up menus allow you to add interactive actions to first responder views.

Recipe 8: Adding Menus to Interactive Views
- (BOOL) canBecomeFirstResponder
{
    // Menus only work with first responders
    return YES;
}

- (void) pressed: (UILongPressGestureRecognizer *) recognizer
{
    if (![self becomeFirstResponder])
    {
        NSLog(@"Could not become first responder");
        return;
    }
    
    UIMenuController *menu = [UIMenuController sharedMenuController];
    UIMenuItem *pop = [[UIMenuItem alloc] 
        initWithTitle:@"Pop" action:@selector(popSelf)];
    UIMenuItem *rotate = [[UIMenuItem alloc] 
        initWithTitle:@"Rotate" action:@selector(rotateSelf)];
    UIMenuItem *ghost = [[UIMenuItem alloc] 
        initWithTitle:@"Ghost" action:@selector(ghostSelf)];
    [menu setMenuItems:@[pop, rotate, ghost]];
    
    [menu setTargetRect:self.bounds inView:self];
    menu.arrowDirection = UIMenuControllerArrowDown;
    [menu update];
    
    [menu setMenuVisible:YES];
}

- (id) initWithImage: (UIImage *) anImage
{
    if (!(self = [super initWithImage:anImage])) return nil;

    self.userInteractionEnabled = YES;   
    UILongPressGestureRecognizer *pressRecognizer = 
        [[UILongPressGestureRecognizer alloc] initWithTarget:self 
            action:@selector(pressed:)];
    [self addGestureRecognizer:pressRecognizer];

    return self;
}

Summary

Even in their most basic form, touch-based interfaces offer easy-to-implement flexibility and power. You've now discovered how to move views around the screen and how to bound that movement. You read about testing touches to see whether views should or should not respond to them. You saw how to "paint" on a view and how to attach recognizers to views to interpret and respond to gestures. Here's a collection of thoughts about the recipes in this two-part article that you might want to ponder before moving on:

  • Be concrete. iOS devices have perfectly good touch screens. Why not let your users drag items around the screen or trace lines with their fingers? It adds to the reality and the platform's interactive nature.
  • Users typically have five fingers per hand. iPads, in particular, offer a lot of screen real estate. Don't limit yourself to a one-finger interface when it makes sense to expand your interaction into Multi-Touch territory for one or more users, screen space allowing.
  • A solid grounding in Quartz graphics and Core Animation will be your friend. Using drawRect:, you can build any kind of custom UIView presentation you want, including text, Bezier curves, scribbles, and so forth.
  • If Cocoa Touch doesn't provide the kind of specialized gesture recognizer you're looking for, write your own. It's not that hard, although it helps to be as thorough as possible when considering the states your custom touch might pass through.
  • Use Multi-Touch whenever possible, especially when you can expand your application to invite more than one user to touch the screen at a time. Don't limit yourself to one-person, one-touch interactions when a little extra programming will open doors of opportunity for multiuser use.
  • Explore! These two articles have only touched lightly on the ways you can use direct manipulation in your applications. Use this material as a jumping-off point to explore the full vocabulary of the UITouch class.

Erica is an expert in iOS development and writes frequently on the topic. She holds a Ph. D. in Computer Science from Georgia Tech's Graphics, Visualization and Usability Center. This article was excerpted from a late draft of her Core iOS 6 Developer's Cookbook, 4th Ed. Although it has been reviewed technically, final presentation and content in the book may vary from what was presented here.

Related Article

Handling Touch Input on iOS6


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video