Chapter 4. Event Handling and Graphics Services

In Chapter 3, you were introduced to some of the basic user interface elements of the iPhone. Many objects support high-level events such as buttonClicked and tableRowSelected to notify the application of certain actions taken by the user. These actions rely on lower-level mouse events provided by the UIView class and a base class underneath it: UIResponder. The UIResponder class provides methods to recognize and handle the basic mouse events that occur when the user taps or drags on the iPhone’s screen. These methods are incorporated into other events created in UIView to detect two-fingered gestures. Higher-level objects, such as tables and alert sheets, take these low-level events and wrap them into even higher-level ones to handle button clicks, row selection, and other types of behavior. All such screen-oriented events are processed using the Graphics Services framework, which provides screen coordinates, fingering information, and other data related to the graphics of the event.

This framework is considered to be low-level, and has since been built on with the more user-friendly touches API and other frameworks. These low-level functions tell the application exactly what has occurred on the screen and provide the information needed to interact with the user. They appear to be used largely in Apple’s own preloaded applications. To receive many of these events, you’ll need to add the following function call to your application:

UIApplicationUseLegacyEvents(YES);

Introduction to Geometric Structures

Before diving into events management, you’ll need a basic understanding of some geometric structures commonly used on the iPhone. You’ve already seen some of these in Chapter 3. The Core Graphics framework provides many general structures to handle graphics-related functions. Among these structures are points, window sizes, and window regions. Core Graphics also provides many C-based functions for creating and comparing these structures.

CGPoint

A CGPoint is the simplest Core Graphics structure, and contains two floating-point values corresponding to horizontal (X) and vertical (Y) coordinates on a display. To create a CGPoint, use the CGPointMake method:

CGPoint point = CGPointMake (320.0, 480.0);

The first value represents X, the horizontal pixel value, and the second Y, the vertical pixel value. These values can also be accessed directly:

float x = point.x;
float y = point.y;

The iPhone’s display resolution is 320×480 pixels. The upper-left corner of the screen is referenced at 0×0, and the lower right at 319×479 (pixel values are zero-indexed).

Being a general-purpose structure, a CGPoint can refer equally well to a coordinate on the screen or within a window. For example, if a window is drawn at 0×240 (halfway down the screen), a CGPoint with values (0, 0) could address either the upper-left corner of the screen or the upper-left corner of the window (0×240). Which one it means is determined by the context where the structure is being used in the program.

Two CGPoint structures can be compared using the CGPointEqualToPoint function:

BOOL isEqual = CGPointEqualToPoint(point1, point2);

CGSize

A CGSize structure represents the size of a rectangle. It encapsulates the width and height of an object, and is primarily found in the iPhone APIs to dictate the size of screen objects, namely windows. To create a CGSize object, use CGSizeMake:

CGSize size = CGSizeMake(320.0, 480.0);

The values provided to CGSizeMake indicate the width and height of the element being described. Values can be directly accessed using the structure’s width and height variable names:

float width = size.width;
float height = size.height;

Two CGSize structures can be compared using the CGSizeEqualToSize function:

BOOL isEqual = CGSizeEqualToSize(size1, size2);

CGRect

The CGRect structure combines a CGPoint and CGSize structure to describe the frame of a window on the screen. The frame includes an origin, which represents the location of the upper-left corner of the window, and the size of the window. To create a CGRect, use the CGRectMake function:

CGRect rect = CGRectMake(0, 200, 320, 240);

This example describes a 320×240 window whose upper-left corner is located at coordinates 0×200. As with the CGPoint structure, these coordinates could reference a point on the screen itself or offsets within an existing window; it depends on where and how the CGRect structure is used.

The components of the CGRect structure can also be accessed directly:

CGPoint windowOrigin = rect.origin;
float x = rect.origin.x;
float y = rect.origin.y;

CGSize windowSize = rect.size;
float width = rect.size.width;
float height = rect.size.height;

Containment and intersection

Two CGRect structures can be compared using the CGRectEqualToRect function:

BOOL isEqual = CGRectEqualToRect(rect1, rect2);

To determine whether a given point is contained inside a CGRect, use the CGRectContainsPoint method. This is particularly useful when determining whether a user has tapped inside a particular region. The point is represented as a CGPoint structure:

BOOL containsPoint = CGRectContainsPoint(rect, point);

A similar function can be used to determine whether one CGRect structure contains another CGRect structure. This is useful when testing whether certain objects overlap:

BOOL containsRect = CGRectContainsRect(rect1, rect2);

To determine whether two CGRect structures intersect, use the CGRectIntersectsRect function:

BOOL doesIntersect = CGRectIntersectsRect(rect1, rect2);

Edge and center detection

The following functions can be used to determine the various edges of a rectangle and calculate the coordinates of the rectangle’s center. All of these functions accept a CGRect structure as their only argument and return a float value:

CGRectGetMinX

Returns the coordinate of the left edge of the rectangle.

CGRectGetMinY

Returns the coordinate of the bottom edge of the rectangle.

CGRectGetMidX

Returns the center X coordinate of the rectangle.

CGRectGetMidY

Returns the center Y coordinate of the rectangle.

CGRectGetMaxX

Returns the coordinate of the right edge of the rectangle.

CGRectGetMaxY

Returns the coordinate of the upper edge of the rectangle.

Introduction to GSEvent

The GSEvent structure is the standard object that describes graphics-level events to a class’s event-handling methods. It can be used in conjunction with the Graphics Services framework to decode details about the event that has occurred.

All event methods receive a pointer to a GSEvent structure when notified of an event. The prototype for an event typically follows this standard:

- (void)eventName: (struct _GSEvent *)event
{
    /* Event handling code */
}

Graphics Services

Whenever an event is received, the object communicates with Graphics Services to get the specifics of the event. The Graphics Services framework provides many different decoding functions to extract the event’s details.

Event location

For one-fingered events, the GSEventGetLocationInWindow function returns a CGPoint structure containing the X, Y coordinates where the event occurred. These coordinates are generally offset to the position of the window that received the event. For example, if a window located at the bottom half of the screen, whose origin was 0×240, received an event at 0×0, this means the event actually took place at 0×240 on the screen, which is where the window’s 0×0 origin is located.

The GSEventGetLocationInWindow method returns a CGPoint structure, which you can unpack as follows:

CGPoint point = GSEventGetLocationInWindow(event);
float x = point.x;
float y = point.y;

If a two-fingered gesture is being used, you must call two separate functions to obtain the window coordinates for each finger. The left-most finger is considered the inner finger, making the right-most finger the outer one. The GSEventGetInnerMostPathPosition and GSEventGetOuterMostPathPosition methods return CGPoint structures containing the X, Y window coordinates for each finger:

CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);

When the iPhone is oriented for landscape mode, these positions are reversed:

int orientation = [ UIHardware deviceOrientation: YES ];

if ( orientation == kOrientationHorizontalLeft
  || orientation == kOrientationHorizontalRight )
{
    leftFinger = GSEventGetOuterMostPathPosition(event);
    rightFinger = GSEventGetInnerMostPathPosition(Event);
}

Event type

The event type identifies whether a single finger or gesture was used, and whether the event involved a finger being placed down or raised from the screen.

Most events can be easily identified through the method that was notified. For example, if a finger is pressed down, this notifies the mouseDown method, whereas a two-finger gesture results in the gestureStarted method being notified:

unsigned int eventType = GSEventGetType(event);

For a one-fingered event, the event type would begin as a single finger down, followed by all fingers up (when the user released). A two-fingered gesture is a little more complex. It begins life with a single finger down, and then changes to a two-finger gesture. If the user lifts one finger, the event type then changes to a one-finge- up event, followed by an all-fingers-up event when the user removes the second finger.

The following events are tied to event type values:

Type

Description

1

One finger down, including first finger of a gesture

2

All fingers up

5

One finger up in a two-fingered gesture

6

Two-finger gesture

Event chording (multiple-finger events)

When more than one note is played on a piano, it’s considered a chord. The same philosophy is used in processing gestures. A single finger represents one note being played on the screen, whereas two are considered a chord.

The GSEventIsChordingHandEvent method can be used to determine how many fingers are down on the screen at the time of the event:

int eventChording = GSEventIsChordingHandEvent(event);

This function returns a value of 0 if the event is a single finger event, or a value of 1 if it is considered a chording event.

Mouse Events

Mouse events are considered to be any touch screen event where a single finger is used. All classes derived from the UIResponder class inherit mouse events, and some classes override these to form new types of events, such as row selection within a table or changing the position of switches and sliders in a control.

To receive notifications for any of the six supported mouse events, create a subclass of the object for which you want to receive events and override its methods. For example, the following class will receive events sent to a UITable object and define custom handling for single-finger actions by the user. This will cause the custom methods to be called whenever the user presses a finger within the table’s window region:

@interface MyTable : UITable
{

}
- (void) mouseDown:(struct _GSEvent *)event;
- (void) mouseUp:(struct _GSEvent *)event;
@end

Because the base class might also take advantage of the mouse event, you’ll want to call the superclass’s version of the method either before or after you’ve processed the event:

- (void) mouseDown:(struct _GSEvent *)event {

    /* Handle mouse down */

    [ super mouseDown: event ];
}

mouseDown

The mouseDown method is called whenever a single finger is pressed on the screen. This includes the first finger of two-fingered gestures before a gesture is started. The event location represents the coordinates at which the screen press occurred within the object’s window:

- (void) mouseDown:(struct _GSEvent *)event {
    CGPoint pointDown = GSEventGetLocationInWindow(event);

    /* Handle mouse down */

    [ super mouseDown: event ];
}

mouseUp

The mouseUp event method is notified whenever the user lifts a finger off of the screen. It is the most appropriate method for performing value checks of controls, such as a segmented control, or of other classes that are not equipped with their own custom notifications for such events. The superclass’s method should be called first, to allow values of controls to change before reading them. The event location represents the last coordinates of the user’s finger before lifting off the screen:

- (void) mouseUp:(struct _GSEvent *)event {
    CGPoint pointUp = GSEventGetLocationInWindow(event);

    [ super mouseUp: event ];

    /* Check value of control, etc. */
}

mouseDragged

If the user continues to hold her finger down after a mouseDown event is sent and if the finger is moved from its original position on the screen, the mouseDragged method is notified. This is the equivalent of a click-and-drag function on the desktop. The event location represents the coordinates to which the user dragged the finger before lifting it:

- (void) mouseDragged:(struct _GSEvent *)event {
    CGPoint movedTo = GSEventGetLocationInWindow(event);

    [ super mouseDragged: event ];

    /* Handle mouse drag */
}

This method tracks with the user’s finger, so it is called at regular intervals as the finger moves.

mouseEntered, mouseExited, mouseMoved

On the desktop, these methods notify the application when the mouse is scrolled into, out of, and within an object’s frame in the absence of any click event. The iPhone doesn’t have a real mouse, so when you tap with your finger it’s treated as a mouse down. These methods are relatively useless for the iPhone, but future mobile devices from Apple might make use of a real mouse or trackball.

Gesture Events

When the user switches from a one-fingered tap to using two fingers, it’s considered the beginning of a gesture. This causes gesture events to be created, which can be intercepted by overriding the appropriate methods. Gestures are provided in the UIView base class, and only objects that inherit from it support them.

The order of events is this: as a gesture is started, the gestureStarted method is invoked. Should the user change his finger position, the gestureChanged method gets called to notify the object of each new gesture position. When the user has ended his gesture, gestureEnded is finally called.

In order for gesture events to be sent, the class must override a method named canHandleGestures, which returns a Boolean value. By returning YES, you tell the iPhone to send events:

- (BOOL)canHandleGestures
{
   return YES;
}

gestureStarted

The gestureStarted method is notified when the user makes screen contact with two fingers or transitions from using one finger to two. This method is the two-fingered version of mouseDown. The inner and outer coordinates correspond to the first point of contact on the screen. The event locations in the CGPoint structures returned represent the coordinates at which each finger was pressed:

- (void)gestureStarted:(struct _GSEvent)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);

    [ super gestureStarted: event ];

    /* Handle gesture started event */
}

gestureEnded

The gestureEnded method is the two-fingered equivalent to mouseUp, and lets the application know that the user has removed at least one finger from the screen. If the user removes both fingers at the same time, the iPhone sends both events to the gestureEnded and mouseUp methods. If the user lifts fingers separately, the first finger to be removed causes a call to gestureEnded and the second causes a call to mouseUp.

The screen coordinates provided with the gestureEnded method identify the point, if any, that remains in a pressed-down state when just one finger was removed and the other finger is still down. When the second finger is removed, the mouseUp method will be notified, as the gesture will have been demoted to a mouse event when the first finger was removed:

- (void)gestureEnded:(struct _GSEvent)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);

    [ super gestureEnded: event ];

    /* Handle gesture ended event */
}

gestureChanged

Whenever the user moves his fingers while in a two-fingered gesture, the gestureChanged method is called. This is the two-fingered equivalent of mouseDragged. When it occurs, the application should reevaluate the finger positions of the gesture and make the appropriate response. The event locations represent the new coordinates to which the fingers have been dragged:

- (void)gestureChanged:(struct _GSEvent)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);

    [ super gestureChanged: event ];

    /* Handle gesture change event */
}

Status Bar Events

The iPhone’s status bar is a window itself, capable of receiving mouse events. The Safari application sets a precedent for the type of behavior that should ensue on such events, which is to have the primary view scroll to the beginning. The UIApplication class contains three status bar notifications that can be overridden in your program.

The statusBarMouseDown event is notified whenever the user taps the status bar:

- (void)statusBarMouseDown:(struct _GSEvent *)event;

If the user leaves her finger down and drags it to a different position, the statusBarMouseDragged method is notified with an event containing the coordinates of the position it has been dragged to on the screen:

- (void)statusBarMouseDragged:(struct _GSEvent *)event;

Finally, when the user lifts her finger, the statusBarMouseUp method is notified with an event containing the coordinates of the last position held by the user’s finger on the screen:

- (void)statusBarMouseUp:(struct _GSEvent *)event;

Example: The Icon Shuffle

This example creates four icons on the screen and allows the user to move them around freely, either individually or two at a time using a gesture. This illustrates the use of various geometry structures and functions, event notifications, and Graphics Services functions. To pick up an icon, tap and hold it, move it where it should go, then release your finger. If the status bar is tapped, the icons are reset to their original positions.

To compile this example from the command line, you’ll need to use several different frameworks—Core Graphics, Graphics Services, and UIKit—in addition to the foundation frameworks:

$ arm-apple-darwin9-gcc -o MyExample MyExample.m -lobjc \
    -framework CoreFoundation -framework Foundation \
    -framework UIKit -framework CoreGraphics \
    -framework GraphicsServices

Examples 4-1 and 4-2 contain the header file and executable methods for the example.

Example 4-1. Mouse and gesture example (MyExample.h)
#import <CoreFoundation/CoreFoundation.h>
#import <UIKit/UIKit.h>
#import <UIKit/UITextView.h>

@interface MainView : UIView
{
    UIImage *images[4];
    CGRect positions[4];
    CGPoint offsets[4];
    int dragLeft, dragRight;
}
- (id)initWithFrame:(struct CGRect)windowRect;
- (void)reInit;
- (void)mouseDown: (struct _GSEvent *)event;
- (void)mouseUp: (struct _GSEvent *)event;
- (void)mouseDragged: (struct _GSEvent *)event;
- (void)gestureStarted: (struct _GSEvent *)event;
- (void)gestureEnded: (struct _GSEvent *)event;
- (void)gestureChanged: (struct _GSEvent *)event;
- (void)drawRect:(CGRect)rect;
@end

@interface MyApp : UIApplication
{
    UIWindow *window;
    MainView *mainView;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void)statusBarMouseDown:(struct _GSEvent *)event;
@end
Example 4-2. Mouse and gesture example (MyExample.m)
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <GraphicsServices/GraphicsServices.h>
#import "MyExample.h"

int main(int argc, char **argv)
{
    NSAutoreleasePool *autoreleasePool = [
        [ NSAutoreleasePool alloc ] init
    ];
    UIApplicationUseLegacyEvents(YES);
    int returnCode = UIApplicationMain(argc, argv, @"MyApp", @"MyApp");
    [ autoreleasePool release ];
    return returnCode;
}

@implementation MyApp
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    window = [ [ UIWindow alloc ] initWithContentRect:
        [ UIHardware fullScreenApplicationContentRect ]
    ];

    CGRect rect = [ UIHardware fullScreenApplicationContentRect ];
    rect.origin.x = rect.origin.y = 0.0f;

    mainView = [ [ MainView alloc ] initWithFrame: rect ];
    [ window setContentView: mainView ];
    [ window orderFront: self ];
    [ window makeKey: self ];
    [ window _setHidden: NO ];

}

- (void)statusBarMouseDown:(struct _GSEvent *)event {
    [ mainView reInit ];
    [ mainView setNeedsDisplay ];
}

@end

@implementation MainView
- (id)initWithFrame:(struct CGRect)windowRect {
    self = [ super initWithFrame: windowRect ];
    if (nil != self) {
        int i;

        images[0] = [ UIImage
          imageAtPath: @"/Applications/MobilePhone.app/icon.png" ];
        images[1] = [ UIImage
          imageAtPath: @"/Applications/MobileMail.app/icon.png" ];
        images[2] = [ UIImage
          imageAtPath: @"/Applications/MobileSafari.app/icon.png" ];
        images[3] = [ UIImage
            imageAtPath:
            @"/Applications/MobileMusicPlayer.app/icon.png" ];

        [ self reInit ];
    }
    return self;
}

- (void)reInit {
        positions[0] = CGRectMake(98, 178, 60, 60);
        positions[1] = CGRectMake(162, 178, 60, 60);
        positions[2] = CGRectMake(98, 242, 60, 60);
        positions[3] = CGRectMake(162, 242, 60, 60);

        dragLeft = dragRight = −1;
}

- (void)drawRect:(CGRect)rect {
    float black[4] = { 0, 0, 0, 1 };
    CGContextRef ctx = UICurrentContext(  );
    int i;

    CGContextSetFillColor(ctx, black);
    CGContextFillRect(ctx, rect);

    for(i=0;i<4;i++) {
        [ images[i] draw1PartImageInRect: positions[i] ];
    }
}

- (void)mouseDown: (struct _GSEvent *)event {
    CGPoint point = GSEventGetLocationInWindow(event);
    int i;

    for(i=0;i<4;i++) {
        if (CGRectContainsPoint(positions[i], point)) {
            dragLeft = i;
            offsets[i] = CGPointMake
              ( point.x - positions[i].origin.x,
                point.y - positions[i].origin.y );
        }
    }
}

- (void)mouseUp: (struct _GSEvent *)event {
    CGPoint point = GSEventGetLocationInWindow(event);
    int i;

    dragLeft = −1;
}

- (void)mouseDragged: (struct _GSEvent *)event {
    CGPoint point = GSEventGetLocationInWindow(event);
    CGRect old;
    int i;

    if (dragLeft != −1) {
        old = positions[dragLeft];
        positions[dragLeft].origin.x = point.x - offsets[dragLeft].x;
        positions[dragLeft].origin.y = point.y - offsets[dragLeft].y;
        [ self setNeedsDisplayInRect: old ];
        [ self setNeedsDisplayInRect: positions[dragLeft] ];
    }
}

- (void)gestureStarted: (struct _GSEvent *)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);
    int i;

    for(i=0;i<4;i++) {
        if (CGRectContainsPoint(positions[i], leftFinger)) {
            dragLeft = i;
            offsets[i] = CGPointMake
              ( leftFinger.x - positions[i].origin.x,
                leftFinger.y - positions[i].origin.y );
        }
        else if (CGRectContainsPoint(positions[i], rightFinger)) {
            dragRight = i;
            offsets[i] = CGPointMake
              ( rightFinger.x - positions[i].origin.x,
                rightFinger.y - positions[i].origin.y );
        }
    }
}

- (void)gestureEnded: (struct _GSEvent *)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);
    int i;

    dragLeft = dragRight = −1;

    for(i=0;i<4;i++) {
        if (CGRectContainsPoint(positions[i], leftFinger))
            dragLeft = i;
        else if (CGRectContainsPoint(positions[i], rightFinger))
            dragRight = i;
    }
}

- (void)gestureChanged: (struct _GSEvent *)event {
    CGPoint leftFinger = GSEventGetInnerMostPathPosition(event);
    CGPoint rightFinger = GSEventGetOuterMostPathPosition(event);
    CGRect old;
    int i;

    if (dragLeft != −1) {
        old = positions[dragLeft];
        positions[dragLeft].origin.x
            = leftFinger.x - offsets[dragLeft].x;
        positions[dragLeft].origin.y
            = leftFinger.y - offsets[dragLeft].y;
        [ self setNeedsDisplayInRect: old ];
        [ self setNeedsDisplayInRect: positions[dragLeft] ];
    }

    if (dragRight != −1) {
        old = positions[dragRight];
        positions[dragRight].origin.x
            = rightFinger.x - offsets[dragRight].x;
        positions[dragRight].origin.y
            = rightFinger.y - offsets[dragRight].y;
        [ self setNeedsDisplayInRect: old ];
        [ self setNeedsDisplayInRect: positions[dragRight] ];
    }
}

- (BOOL)canHandleGestures {

   return YES;
}

@end

What’s Going On

Here’s how the icon shuffle works:

  1. When the application instantiates, a MainView object is created, which is derived from UIView, and its initWithFrame method is called. This initializes the images and positions of four icons on the screen: Phone, Mail, Safari, and iPod. The view class is then told to display itself.

  2. As the view class is drawn, the class’s drawRect method is called. This causes a black rectangle to first be rendered to blank the screen’s background. Next, each icon is individually rendered on the screen using methods from the UIImage class (discussed more in Chapter 7).

  3. When a single finger is used to move an icon, the mouseDown method is first called. This checks to see which icon the user has pressed and sets it in the object’s dragLeft variable as the actively moving icon. The delta between where the user pressed inside the icon and the upper-left corner (origin) of the icon is stored so that the example can track the exact part of the icon that was pressed with the user’s finger.

  4. When the user’s finger moves, the mouseDragged method is called, which sets the icon position to the current finger position, adjusting for where the user actually pressed inside the icon. This way, if the user pressed the center of the icon, the icon is moved so that its center will track with the user’s finger. The setNeedsDisplayInRect method is called to invoke the view class’s drawRect method again.

  5. When the user’s finger lifts up, the mouseUp method is called, which resets the active icon.

  6. If two fingers are used, the gesture methods perform the same tasks, but process both finger positions, allowing two icons to track with the user’s fingers. Because you don’t know whether the fingers will be put down and raised in the same order, separate information must be stored on the left and right fingers. Note that putting down a second finger causes the information saved by the mouseDown method to be thrown away and replaced by the information returned by the the gestureStarted method. Similarly, raising the second finger causes the information saved by the gestureChanged method to be thrown away and replaced by the information returned by the mouseDragged method.

Further Study

It’s a good idea to explore all of the different methods supported in these lower-level classes. Check out the following prototypes in your tool chain’s include directory. These can be found in /toolchain/sys/usr/include: UIKit/UIResponder.h, UIKit/UIView.h, GraphicsServices/GraphicsServices.h, and CoreGraphics/CGGeometry.h.

Get iPhone Open Application Development, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.