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 |
| One finger down, including first finger of a gesture |
| All fingers up |
| One finger up in a two-fingered gesture |
| 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.
#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
#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:
When the application instantiates, a
MainView
object is created, which is derived fromUIView
, and itsinitWithFrame
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.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 theUIImage
class (discussed more in Chapter 7).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’sdragLeft
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.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. ThesetNeedsDisplayInRect
method is called to invoke the view class’sdrawRect
method again.When the user’s finger lifts up, the
mouseUp
method is called, which resets the active icon.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 thegestureStarted
method. Similarly, raising the second finger causes the information saved by thegestureChanged
method to be thrown away and replaced by the information returned by themouseDragged
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.