Channels ▼
RSS

Design

Gestures and Touches in iOS6: More Recipes


Touch-based interfaces offer easy-to-implement flexibility and power. Part 1 of this two-part series explored ways to help you build applications that work directly with user gestures and go far beyond the capabilities of prebuilt controls for direct manipulation interfaces. In this final part, I examine touch-based painting, smoothing drawing paths, and building in the capability for Multi-Touch interfaces, circle detection, custom-gesture recognition, and other features that will expand your application's ability to work with user gestures and touches.

Drawing Touches Onscreen

UIView hosts the realm of direct onscreen drawing. Its drawRect method offers a low-level way to draw content directly, letting you create and display arbitrary elements using Quartz 2D calls. Touch plus drawing join together to build concrete interfaces that can easily be manipulated in a variety of ways.

Recipe 1 combines gestures with drawRect to introduce touch-based painting. As a user touches the screen, the TouchTrackerView class builds a Bezier path that follows the user's finger. To paint the progress as the touch proceeds, the touchesMoved:withEvent: method calls setNeedsDisplay. This, in turn, triggers a call to drawRect:, where the view strokes the accumulated Bezier path. Figure 1 shows the interface with a path created in this way.


Figure 1: A simple painting tool for iOS requires little more than collecting touches along a path and painting that path with UIKit/Quartz 2D calls.

Although you could adapt this recipe to use gesture recognizers, there's really no need. The touches are essentially meaningless, and are only provided to create a pleasing tracing. The basic responder methods (namely, touches began, moved, and so on) are perfectly capable of handling path creation and management tasks.

This example is meant for creating continuous traces. It does not respond to any touch event without a move; if you want to expand this recipe to add a simple dot or mark, you'll have to add that behavior yourself.

Recipe 1: Touch-Based Painting in a UIView.
@interface TouchTrackerView : UIView
{
    UIBezierPath *path;
}
@end

@implementation TouchTrackerView
- (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event
{
    // Initialize a new path for the user gesture
    self.path = [UIBezierPath bezierPath];
    path.lineWidth = 4.0f;

    UITouch *touch = [touches anyObject];
    [path moveToPoint:[touch locationInView:self]];
}

- (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event
{
    // Add new points to the path
    UITouch *touch = [touches anyObject];
    [self.path addLineToPoint:[touch locationInView:self]];
    [self setNeedsDisplay];
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
        UITouch *touch = [touches anyObject];
        [path addLineToPoint:[touch locationInView:self]];
        [self setNeedsDisplay];
}

- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
        [self touchesEnded:touches withEvent:event];
}

- (void) drawRect:(CGRect)rect
{
    // Draw the path
    [path stroke];
}

- (id) initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
        self.multipleTouchEnabled = NO;
    return self;
}
@end

The full sample project for Recipe 1 and all other recipes in this article are available online (navigate to the first folder to find them.)

Smoothing Drawings

Depending on the device in use and the amount of simultaneous processing involved, capturing user gestures may produce results that are rougher than desired. Touch events are often limited by CPU demands as well as by shaking hands. A smoothing algorithm can offset those limitations by interpolating between points. Figure 2 demonstrates the kind of angularity that derives from granular input and the smoothing that can be applied instead.


Figure 2: Catmull-Rom smoothing can be applied in real time to improve arcs between touch events. The images shown here are based on an identical gesture input, with and without smoothing applied.

Catmull-Rom splines create continuous curves between key points. This algorithm ensures that each initial point you provide remains part of the final curve. The resulting path retains the original path's shape. You choose the number of interpolation points between each pair of reference points. The trade-off lies between processing power and greater smoothing. The more points you add, the more CPU resources you'll consume. As you'll see when using the sample code that accompanies this article, a little smoothing goes a long way, even on newer devices; the latest iPad is so responsive that it's hard to draw a particularly jaggy line in the first place.

Recipe 2 demonstrates how to extract points from an existing Bezier path and then apply splining to create a smoothed result. Catmull-Rom uses four points at a time to calculate intermediate values between the second and third points, using a granularity you specify between those points.

Recipe 2 provides an example of just one kind of real-time geometric processing you might add to your applications. Many other algorithms out there in the world of computational geometry can be applied in a similar manner.

Recipe 2:Creating Smoothed Bezier Paths Using Catmull-Rom Splining.
#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]]

@implementation UIBezierPath (Points)
void getPointsFromBezier(void *info, const CGPathElement *element) 
{
    NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;    
    
    // Retrieve the path element type and its points
    CGPathElementType type = element->type;
    CGPoint *points = element->points;
    
    // Add the points if they're available (per type)
    if (type != kCGPathElementCloseSubpath)
    {
        [bezierPoints addObject:VALUE(0)];
        if ((type != kCGPathElementAddLineToPoint) &&
            (type != kCGPathElementMoveToPoint))
            [bezierPoints addObject:VALUE(1)];
    }    
    if (type == kCGPathElementAddCurveToPoint)
        [bezierPoints addObject:VALUE(2)];
}

- (NSArray *)points
{
    NSMutableArray *points = [NSMutableArray array];
    CGPathApply(self.CGPath, (__bridge void *)points, getPointsFromBezier);
    return points;
}
@end

#define POINT(_INDEX_) \
    [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue]

@implementation UIBezierPath (Smoothing)
- (UIBezierPath *) smoothedPath: (int) granularity
{
    NSMutableArray *points = [self.points mutableCopy];
    if (points.count < 4) return [self copy];
    
    // Add control points to make the math make sense
    // Via Josh Weinberg
    [points insertObject:[points objectAtIndex:0] atIndex:0];
    [points addObject:[points lastObject]];
    
    UIBezierPath *smoothedPath = [UIBezierPath bezierPath];  
    
    // Copy traits
    smoothedPath.lineWidth = self.lineWidth;
    
    // Draw out the first 3 points (0..2)
    [smoothedPath moveToPoint:POINT(0)];
    
    for (int index = 1; index < 3; index++)
        [smoothedPath addLineToPoint:POINT(index)];
    
    for (int index = 4; index < points.count; index++)
    {
        CGPoint p0 = POINT(index - 3);
        CGPoint p1 = POINT(index - 2);
        CGPoint p2 = POINT(index - 1);
        CGPoint p3 = POINT(index);
        
        // now add n points starting at p1 + dx/dy up 
        // until p2 using Catmull-Rom splines
        for (int i = 1; i < granularity; i++)
        {
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;
            
            CGPoint pi; // intermediate point
            pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + 
               (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
            pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + 
               (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
            [smoothedPath addLineToPoint:pi];
        }
        
        // Now add p2
        [smoothedPath addLineToPoint:p2];
    }
    
    // finish by adding the last point
    [smoothedPath addLineToPoint:POINT(points.count - 1)];
    
    return smoothedPath;
}

@end

// Example usage:
// Replace the path with a smoothed version after drawing completes
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    [path addLineToPoint:[touch locationInView:self]];
    path = [path smoothedPath:4];
    [self setNeedsDisplay];
}

Using Multi-Touch Interaction

Enabling Multi-Touch interaction in UIView instances lets iOS recover and respond to more than one finger touch at a time. Set the UIView property multipleTouchEnabled to YES or override isMultipleTouchEnabled for your view. When enabled, each touch callback returns an entire set of touches. When that set's count exceeds 1, you know you're dealing with Multi-Touch.

In theory, iOS supports an arbitrary number of touches. You can explore that limit by running the following recipe on an iPad, using as many fingers as possible at once. The practical upper limit has changed over time; this recipe modestly demurs from offering a specific number.

When Multi-Touch was first explored on the iPhone, developers did not dream of the freedom and flexibility that Multi-Touch combined with multiple users offered. Adding Multi-Touch to your games and other applications opens up not just expanded gestures, but also new ways of creating profoundly exciting multiuser experiences, especially on larger screens like the iPad. I encourage you to include Multi-Touch support in your applications wherever it is practical.


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.
 

Comments:

Video