iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (66 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
7.63Mb size Format: txt, pdf, ePub
Touch Events

As a subclass of
UIResponder
, your view can override four methods to handle the four distinct touch events:

 
  • a finger or fingers touches the screen
 
- (void)
touchesBegan:
(NSSet *)touches
          
withEvent:
(UIEvent *)event;
  
 
  • a finger or fingers move across the screen (This message is sent repeatedly as a finger moves.)
 
- (void)
touchesMoved:
(NSSet *)touches
          
withEvent:
(UIEvent *)event;
  
 
  • a finger or fingers are removed from the screen
 
- (void)
touchesEnded:
(NSSet *)touches
          
withEvent:
(UIEvent *)event;
  
 
  • a system event, like an incoming phone call, interrupts a touch before it ends
 
- (void)
touchesCancelled:
(NSSet *)touches
              
withEvent:
(UIEvent *)event;
  
 

When a finger touches the screen, an instance of
UITouch
is created. The
UIView
that this finger touched is sent the message
touchesBegan:withEvent:
and the
UITouch
is in the
NSSet
of
touches
.

 

As that finger moves around the screen, the touch object is updated to contain the current location of the finger on the screen. Then, the same
UIView
that the touch began on is sent the message
touchesMoved:withEvent:
. The
NSSet
that is passed as an argument to this method contains the same
UITouch
that originally was created when the finger it represents touched the screen.

 

When a finger is removed from the screen, the touch object is updated one last time to contain the current location of the finger, and the view that the touch began on is sent the message
touchesEnded:withEvent:
. After that method finishes executing, the
UITouch
object is destroyed.

 

From this information, we can draw a few conclusions about how touch objects work:

 
  • One
    UITouch
    corresponds to one finger on the screen. This touch object lives as long as the finger is on the screen and always contains the current position of the finger on the screen.
 
  • The view that the finger started on will receive every touch event message for that finger no matter what. If the finger moves outside of the
    UIView
    ’s
    frame
    that it began on, that view still receives the
    touchesMoved:withEvent:
    and
    touchesEnded:withEvent:
    messages. Thus, if a touch begins on a view, then that view owns the touch for the life of the touch.
 
  • You don’t have to – nor should you ever – keep a reference to a
    UITouch
    object. The application will give you access to a touch object when it changes state.
 

Every time a touch does something, like begins, moves, or ends, a
touch event
is added to a queue of events that the
UIApplication
object manages. In practice, the queue rarely fills up, and events are delivered immediately. The delivery of these touch events involves sending one of the
UIResponder
messages to the view that owns the touch. (If your touches are sluggish, then one of your methods is hogging the CPU, and events are waiting in line to be delivered.
Chapter 21
will show you how to catch these problems.)

 

What about multiple touches? If multiple fingers do the same thing at the exact same time to the same view, all of these touch events are delivered at once. Each touch object – one for each finger – is included in the
NSSet
passed as an argument in the
UIResponder
messages. However, the window of opportunity for the

exact same time

is fairly short. So, instead of one responder message with all of the touches, there are usually multiple responder messages with one or more of the touches.

 
Creating the TouchTracker Application

Now let’s get started with your application. In
Xcode
, create a new
Empty Application
iPhone project and name it
TouchTracker
. Don’t specify a
Class Prefix
and only check the box for
Automatic Reference Counting
.

 

First, you will need a model object that describes a line. Create a new
NSObject
and name it
Line
. In
Line.h
, declare two
CGPoint
properties:

 
#import
@interface Line : NSObject
@property (nonatomic) CGPoint begin;
@property (nonatomic) CGPoint end;
@end

In
Line.m
, synthesize the properties:

 
#import "Line.h"
@implementation Line
@synthesize begin, end;
@end
 

Next, create a new
NSObject
called
TouchDrawView
. In
TouchDrawView.h
, change the superclass to
UIView
. Also, declare two collection objects: an array to hold complete lines and a dictionary to hold lines that are still being drawn. You’ll see why we’re using different collection objects shortly.

 
#import
@interface TouchDrawView : NSObject
@interface TouchDrawView : UIView
{
    NSMutableDictionary *linesInProcess;
    NSMutableArray *completeLines;
}
- (void)clearAll;
@end
 

Now you need a view controller to manage an instance of
TouchDrawView
in
TouchTracker
. Create a new
NSObject
subclass named
TouchViewController
. In
TouchViewController.h
, change the superclass to
UIViewController
.

 
@interface TouchViewController : NSObject
@interface TouchViewController : UIViewController
 

In
TouchViewController.m
, override
loadView
to set up an instance of
TouchDrawView
as
TouchViewController
’s
view
. Make sure to import the header file for
TouchDrawView
at the top of this file.

 
#import "TouchViewController.h"
#import "TouchDrawView.h"
@implementation TouchViewController
    
- (void)loadView
{
    [self setView:[[TouchDrawView alloc] initWithFrame:CGRectZero]];
}
@end
 

In
AppDelegate.m
, create an instance of
TouchViewController
and set it as the
rootViewController
of the window. Don’t forget to import the header file for
TouchViewController
in this file.

 
#import "AppDelegate.h"
#import "TouchViewController.h"
@implementation AppDelegate
@synthesize window = _window;
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    TouchViewController *tvc = [[TouchViewController alloc] init];
    [[self window] setRootViewController:tvc];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
 
Drawing with TouchDrawView

TouchDrawView
will keep track of all of the lines that have been drawn and any that are currently being drawn. In
TouchDrawView.m
, create the two collections and import the header for the
Line
class.

 
#import "TouchDrawView.h"
#import "Line.h"
@implementation TouchDrawView
- (id)initWithFrame:(CGRect)r
{
    self = [super initWithFrame:r];
    if (self) {
        linesInProcess = [[NSMutableDictionary alloc] init];
        // Don't let the autocomplete fool you on the next line,
        // make sure you are instantiating an NSMutableArray
        // and not an NSMutableDictionary!
        completeLines = [[NSMutableArray alloc] init];
        [self setBackgroundColor:[UIColor whiteColor]];
        [self setMultipleTouchEnabled:YES];
    }
    return self;
}

Notice that you explicitly enabled multi-touch events by sending the message
setMultipleTouchEnabled:
. Without this, only one touch at a time can be active on a view. If another finger touches the view, it will be ignored, and the view will not be sent
touchesBegan:withEvent:
or any of the other
UIResponder
messages.

 

Now override the
drawRect:
method to create lines using functions from Core Graphics:

 
- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 10.0);
    CGContextSetLineCap(context, kCGLineCapRound);
    // Draw complete lines in black
    [[UIColor blackColor] set];
    for (Line *line in completeLines) {
        CGContextMoveToPoint(context, [line begin].x, [line begin].y);
        CGContextAddLineToPoint(context, [line end].x, [line end].y);
        CGContextStrokePath(context);
    }
    // Draw lines in process in red (Don't copy and paste the previous loop;
    // this one is way different)
    [[UIColor redColor] set];
    for (NSValue *v in linesInProcess) {
        Line *line = [linesInProcess objectForKey:v];
        CGContextMoveToPoint(context, [line begin].x, [line begin].y);
        CGContextAddLineToPoint(context, [line end].x, [line end].y);
        CGContextStrokePath(context);
    }
}
 

Finally, write a method that will clear the collections and redraw the view in
TouchDrawView.m
.

 
- (void)clearAll
{
    // Clear the collections
    [linesInProcess removeAllObjects];
    [completeLines removeAllObjects];
    // Redraw
    [self setNeedsDisplay];
}
 
Turning Touches Into Lines

A line (remember 9th grade geometry class?) is defined by two points. Our
Line
stores these points as properties named
begin
and
end
. When a touch begins, you’ll create a line and set both
begin
and
end
to the point where the touch began. When the touch moves, you will update
end
. When the touch ends, you will have your complete line.

 

There are two collection objects that hold
Line
instances. Lines that have been completed are stored in the
completeLines
array. Lines that are still being drawn, however, are stored in an
NSMutableDictionary
. Why do we need a dictionary? We’ve enabled multi-touch, so a user can draw more than one line at a time. This means we have to keep track of which touch events go with which line. For instance, imagine the user touches the screen with two fingers creating two instances of
Line
. Then one of those fingers moves. The
TouchDrawView
is sent a message for the event, but how can it know which line to update?

 

This is why we’re using a dictionary instead of an array for the lines in process. When a touch begins, we will grab the address of the
UITouch
object that is passed in and wrap it in an
NSValue
instance. A new
Line
will be created and added to the dictionary, and the
NSValue
will be its key. As we receive more touch events, we can use the address of the
UITouch
that is passed in to access and update the right line (
Figure 19.2
).

 

Figure 19.2  Object diagram for TouchTracker

 

Now let’s return to the methods for handling touch events. First, in
TouchDrawView.m
, override
touchesBegan:withEvent:
to create a new
Line
instance and store it in an
NSMutableDictionary
.

 
- (void)touchesBegan:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    for (UITouch *t in touches) {
        // Is this a double tap?
        if ([t tapCount] > 1) {
            [self clearAll];
            return;
        }
        // Use the touch object (packed in an NSValue) as the key
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        // Create a line for the value
        CGPoint loc = [t locationInView:self];
        Line *newLine = [[Line alloc] init];
        [newLine setBegin:loc];
        [newLine setEnd:loc];
        // Put pair in dictionary
        [linesInProcess setObject:newLine forKey:key];
    }
}
 

Next, in
TouchDrawView.m
, override
touchesMoved:withEvent:
to update the end point of the line associated with the moving touch.

 
- (void)touchesMoved:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    // Update linesInProcess with moved touches
    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        // Find the line for this touch
        Line *line = [linesInProcess objectForKey:key];
        // Update the line
        CGPoint loc = [t locationInView:self];
        [line setEnd:loc];
    }
    // Redraw
    [self setNeedsDisplay];
}
 

When a touch ends, you need to finalize the line. However, a touch can end for two reasons: the user lifts the finger off the screen (
touchesEnded:withEvent:
) or the operating system interrupts your application (
touchesCancelled:withEvent:
). A phone call, for example, will interrupt your application.

 

In many applications, you’ll want to handle these two events differently. However, for
TouchTracker
, you will write one method to handle both cases. Declare a new method in
TouchDrawView.h
.

 
- (void)endTouches:(NSSet *)touches;

In
TouchDrawView.m
, implement
endTouches:
.

 
- (void)endTouches:(NSSet *)touches
{
    // Remove ending touches from dictionary
    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        Line *line = [linesInProcess objectForKey:key];
        // If this is a double tap, 'line' will be nil,
        // so make sure not to add it to the array
        if (line) {
            [completeLines addObject:line];
            [linesInProcess removeObjectForKey:key];
        }
    }
    // Redraw
    [self setNeedsDisplay];
}

Finally, override the two methods from
UIResponder
to call
endTouches:
in
TouchDrawView.m
.

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

Build and run the application. Then make beautiful line art with one or more fingers.

 

Other books

Blame It on the Champagne by Nina Harrington
The Wanting Seed by Anthony Burgess
Succubus, Interrupted by Jill Myles
Futility by William Gerhardie
Eating Memories by Patricia Anthony
Sonnet to a Dead Contessa by Gilbert Morris
Curtain Up by Lisa Fiedler
A Mummers' Play by Jo Beverley