O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Miscellaneous Hacks and Recipes: Appendix A - iPhone Open Application Development

by Jonathan Zdziarski

Only the major iPhone frameworks have been covered in this book. Dozens of smaller, proprietary frameworks exist on the iPhone waiting to be explored. Through a little bit of hacking, some of these smaller frameworks have proven themselves useful for one-off needs. A number of other interesting recipes have also been concocted for performing tasks such as initiating a phone call or setting the iPhone’s vibrator. This chapter covers some of the hacks and recipes we couldn’t fit anywhere else in the book.

iPhone Open Application Development book cover

This excerpt is from iPhone Open Application Development. This new edition of iPhone Open Application Development covers the latest version of the open toolkit -- now updated for Apple's iPhone 2.x software and iPhone 3G -- and explains in clear language how to create applications using Objective-C and the iPhone API.

buy button

Dumping the Screen

The UIApplication class sports a method named _dumpScreenContents that can be used to make a screenshot on the iPhone. The method causes a file named /tmp/foo_0.png to be written to disk.

To dump the screen contents from within your application, make sure to call the _dumpScreenContents method from your UIApplication class:

[ self _dumpScreenContents: nil ];

You’ll need to have at least a main window with a content view created to take a screenshot. Every call to _dumpScreenContents causes the file to be written to the same location, /tmp/foo_0.png, so be sure to move it out of the way if you’re taking multiple screenshots.

Example: Command-Line Screen Capture Utility

This example takes a snapshot of the screen from the command line, allowing the user to log in via SSH and capture the currently running application. Only iPhone firmware versions 1.x are able to take screenshots from the command line. Versions 2.x and newer can only take screenshots from within your application, so you’ll need to integrate this code into your project if you’re running v2.x. The example creates an invisible window and view that allows the current application on the screen to show through. When run, the example immediately dumps the screen contents into a file named /tmp/foo_0.png.

To compile this application, use the tool chain on the command line as follows:

$ arm-apple-darwin9-gcc -o ScreenDump ScreenDump.m -lobjc \
    -framework UIKit -framework CoreFoundation -framework Foundation

To use this application, first copy it over to the iPhone using SCP. Don’t forget to sign, if necessary, using ldid:

$ scp ScreenDump root@iphone:/usr/bin

SSH into the iPhone and run it from the command line:

$ ssh -l root iphone
# /usr/bin/ScreenDump

Example A.1, “Screen shot example (ScreenDump.m)” contains the code.

Example A.1. Screen shot example (ScreenDump.m)

#import <UIKit/UIKit.h>

@interface ScreenDumpApp : UIApplication
{
    UIWindow *window;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
@end

@implementation ScreenDumpApp
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    CGRect rect = [ UIHardware fullScreenApplicationContentRect ];

    window = [ [ UIWindow alloc ] initWithContentRect: rect ];
    [ window orderFront: self ];
    [ window makeKey: self ];
    [ window _setHidden: YES ];
    [ window setContentView: [
        [ UIView alloc ] initWithFrame: rect ]
    ];

    printf("Dumping screen contents...\n");
    [ self _dumpScreenContents: nil ];
    [ self terminate ];
}

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

@end

What’s Going On

Here’s how the screenshot example works:

  1. When the application instantiates, a UIWindow and UIView object are immediately created. The window’s _setHidden method is used to keep the window hidden.

  2. The UIApplication class’s instance method _dumpScreenContents is called, which dumps the contents of the screen into a file named /tmp/foo_0.png.

  3. The application then calls its own terminate method, effectively killing itself.

Dumping the UI Hierarchy

In the absence of a fully functional debugger for the iPhone, one aid to developers is the _dumpUIHierarchy method provided by the UIApplication class. A UI dump shows the associations of all display UI objects to each other in a parent/child type of hierarchy. For example, a navigation bar may appear like this in the dump:

<dict>
        <key>CGRect</key>
        <data>
        AAAAAAAAoEEAAKBDAABAQg==
        </data>
        <key>Children</key>
        <array>
                <dict>
                        <key>CGRect</key>
                        <data>
                        AAAAAAAAoEEAAAAAAAAAAA==
                        </data>
                        <key>Enabled</key>
                        <false/>
                        <key>ID</key>
                        <string>&lt;UINavigationItemView: 0x22fe60&gt;</string>
                </dict>
                <dict>
                        <key>CGRect</key>
                        <data>
                        AAB3QwAA+EEAAIhCAADwQQ==
                        </data>
                        <key>Enabled</key>
                        <true/>
                        <key>ID</key>
                        <string>BTN Settings</string>
                </dict>
        </array>
        <key>Enabled</key>
        <true/>
        <key>ID</key>
        <string>&lt;UINavigationBar: 0x22f380&gt;</string>
</dict>

To invoke a dump, call the UIApplication class’s _dumpUIHierarchy instance method:

[ self _dumpUIHierarchy: nil ];

An XML-formatted file will be written to /tmp/UIDump. All windowed objects are written to the dump in the hierarchy in which they are currently allocated.

Invoking Safari

Occasionally, it may be appropriate to call Safari to bring up a web page for your application; for example, when the user presses a donate or home page button in your application’s credits page. The UIApplication class supports an openURL method that can be used to seamlessly launch Safari and load a web page in a new window.

To use this, your application needs to create an NSURL object, which is a Cocoa foundation object to store a URL. The NSURL object is passed to the application’s openURL method, where the application framework processes and launches the appropriate handler application:

NSURL *url;
url = [ [ NSURL alloc ] initWithString: @"http://www.oreilly.com" ];
[ self openURL: url ];

Initiating Phone Calls

As was demonstrated in the last section, the openURL method calls Safari to launch website URLs. What’s actually going on is this: each protocol is associated with a specific handler application. As was the case with our last demonstration, URLs beginning with http:// and https:// are associated with Safari and cause it to be opened whenever openURL is called using those protocol prefixes. Just as openURL can be used to open websites in Safari, it can also be used to place phone calls. This is done by using the protocol prefix of tel://:

NSURL *url = [ [ NSURL alloc ]
    initWithString: @"tel://212-555-1234" ];
[ self openURL: url ];

When the openURL method is used on a URL beginning with tel://, the phone application will be launched and the call will be automatically placed. Do try and ensure that your application doesn’t have any bugs and accidentally places expensive overseas calls or prank calls to the White House.

Vibrating

The iPhone includes a built-in vibrating motor for silently notifying the user of new events. This is controlled by the MeCCA framework, which is a private C++ framework used for low-level communications with various devices including audio, Bluetooth, and other hardware. Daniel Peebles has written a low-level vibration example, which can be wrapped into an application or called as a standalone binary.

To vibrate the iPhone, create an instance of the MeCCA_Vibrator C++ class. The prototype for this class looks like this:

class MeCCA_Vibrator {
public:
    int getDurationMinMax(unsigned int&, unsigned int&);
    int activate(unsigned short);
    int activate(unsigned int, unsigned short);
    int activate(unsigned int, unsigned int, unsigned short);
    int deactivate(  );
};

To use this class, first create a new instance of MeCCA_Vibrator:

MeCCA_Vibrator *v = new MeCCA_Vibrator;

The vibrator object’s activate and deactivate methods can then be used to control vibration:

v->activate(1);
usleep(DURATION);
v->deactivate(  );

When calling the usleep( ) function, specify the duration, in microseconds, that you would like the vibrator to run for.

To tap into the MeCCA_Vibrator object, your application must be linked to the MeCCA framework. Using the tool chain, MeCCA can be linked to your application by adding the -framework MeCCA argument to the compiler arguments we described in Chapter 2, Getting Started with Applications:

$ arm-apple-darwin9-gcc -o MyApp MyApp.m -lobjc \
    -framework CoreFoundation \
    -framework Foundation \
    -framework MeCCA

To add this option to the sample makefile from the previous chapter, add the MeCCA framework to the linker flags section so that the library is linked in:

LDFLAGS =    -lobjc \
        -framework CoreFoundation \
        -framework Foundation \
        -framework MeCCA

Transparent Views

Chapter 7, Advanced UIKit Design introduced the UICompositeImageView class, which allowed multiple images to be layered on top of each other, adding layers of transparency to create overlays. The UIView class provides a similar function called setAlpha, allowing a view’s transparency to be adjusted:

[ mainView setAlpha: 0.5 ];

This can be useful when superimposing multiple views, such as creating semitransparent buttons and navigation bars, to allow text or images to remain visible through the object. The alpha value ranges from 0 to 1, where 0 makes a view totally invisible and 1 makes the view completely cover whatever is below.

To use this, create two views using overlapping frames. Set the alpha level of the front view using setAlpha. Now, add the first view followed by the second to your controlling view. The frontmost view should be semi-transparent.

Cover Flow-Style Album Flipping

Chapter 5, Advanced Graphics Programming with Core Surface and Quartz Core covered Layer Kit transformations, which allow a layer to be rotated, scaled, and transformed in many other ways. Layer Kit is the foundation for Apple’s Cover Flow technology, which is used in selecting albums from the iPod application while in landscape mode.

Layton Duncan of Polar Bear Farm, a software designer for the iPhone, has graciously provided code adapting Apple’s CovertFlow example (from Xcode tools’ examples) to the iPhone. The application’s source and images can be downloaded in their entirety at the Polar Bear Farm website, http://www.polarbearfarm.com.

We’ve spiced up Layton’s example a bit to use the iPhone’s photo album as the album covers. So be sure to snap a few photos with your iPhone before trying this example.

To compile this example, use the tool chain on the command line as follows:

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

Examples A.2 and A.3 contain the code.

Example A.2. QuartzCore album example (CovertFlow.h)

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <UIKit/UIKit.h>
#import <UIKit/UIApplication.h>
#import <UIKit/UIScroller.h>
#import <UIKit/UIView-Hierarchy.h>
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CAScrollLayer.h>
#import <GraphicsServices/GraphicsServices.h>

/* Number of pixels scrolled before next cover comes front */
#define SCROLL_PIXELS 60.0

/* Size of each cover */
#define COVER_WIDTH_HEIGHT 128.0

@interface CFView : UIScroller
{
    BOOL beating;
}
- (id) initWithFrame:(struct CGRect)frame;
- (void) mouseDragged:(GSEvent*)event;
- (void) heartbeatCallback;
@end

@interface CovertFlowApplication : UIApplication
{
    UIWindow *window;
    CFView *mainView;
    CAScrollLayer *cfIntLayer;
    NSMutableArray *pictures;
    int selected;
}
+ (CovertFlowApplication *)sharedInstance;
- (void) jumpToCover:(int)index;
- (void) layoutLayer:(CAScrollLayer *)layer;
@end

Example A.3. QuartzCore album example (CovertFlow.m)

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <UIKit/CDStructures.h>
#import <UIKit/UIWindow.h>
#import <UIKit/UIView.h>
#import <UIKit/UIView-Hierarchy.h>
#import <UIKit/UIHardware.h>
#import <UIKit/UIResponder.h>
#import <GraphicsServices/GraphicsServices.h>
#import <UIKit/UIView-Geometry.h>
#import <CoreGraphics/CGGeometry.h>
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CALayer.h>
#import <QuartzCore/CAScrollLayer.h>
#import <QuartzCore/CAAnimation.h>
#import <QuartzCore/CATransition.h>
#import <QuartzCore/CATransaction.h>
#import <QuartzCore/CAMediaTimingFunction.h>
#import "CovertFlow.h"
#import "math.h"

static CovertFlowApplication *sharedInstance;

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

@implementation CFView
-(id)initWithFrame:(struct CGRect)frame
{
    self = [ super initWithFrame: frame ];
    if (nil != self) {
        [self setTapDelegate:self];
        [self setDelegate:self];
        beating = NO;
        [ self startHeartbeat: @selector(heartbeatCallback) inRunLoopMode:nil ];
    }
    return self;
}

- (void) mouseDragged:(GSEvent*)event
{
    if (beating == NO) {

       /* User started flicking through covers.
        * Start a heartbeat to update the coverflow
        */

        beating = YES;
        [ self startHeartbeat: @selector(heartbeatCallback) inRunLoopMode:nil ];
    }
    [ super mouseDragged: event ];
}

- (void)heartbeatCallback
{
    [ [ CovertFlowApplication sharedInstance ]
       jumpToCover:(int) roundf(([ self offset ].y/SCROLL_PIXELS))
    ];

    if (! [self isScrolling] )
    {
        /* Stop the heartbeat when scrolling stops */
        [ self stopHeartbeat: @selector(heartbeatCallback) ];
        beating = NO;
    }
}
@end

@implementation CovertFlowApplication
+ (CovertFlowApplication *)sharedInstance
{
    if (!sharedInstance) {
        sharedInstance = [ [ CovertFlowApplication alloc ] init ];
    }
    return sharedInstance;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    UIImageView *background;
    CGRect rect;
    NSString *file, *path;
    NSDirectoryEnumerator *dirEnum;
    int i, j;

    /* Read the pictures directory */
    path = [ [ NSString alloc ] initWithString:
       @"/var/mobile/Media/DCIM/100APPLE" ];
    pictures = [ [ NSMutableArray alloc] init ];
    dirEnum = [ [ NSFileManager defaultManager ] enumeratorAtPath: path ];
    while ((file = [ dirEnum nextObject ])) {
        if ( [ [ file pathExtension ] isEqualToString: @"THM" ])
        {
            [ pictures addObject: [ [ NSString alloc ] initWithString:
                [ path stringByAppendingPathComponent: file ] ] ];
        }
    }
    j = [ pictures count ];

    window = [ [ UIWindow alloc ] initWithContentRect:
        [ UIHardware fullScreenApplicationContentRect ] ];

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

    sharedInstance = self;
    mainView = [ [ CFView alloc ] initWithFrame: rect ];

    /* Set the # of pixels to drag before the scroller moves */
    [ mainView setScrollHysteresis: 64.0 ];

    /* Disable rubber banding */
    [ mainView setAllowsFourWayRubberBanding: NO ];

    /* Cause the scroller to become stiffer */
    [ mainView setScrollDecelerationFactor: 0.9999 ];

    /* Set the scroll view to snap to pixel boundaries */
    [ mainView setGridSize:CGSizeMake(SCROLL_PIXELS, SCROLL_PIXELS) ];

    [ window setContentView: mainView ];
    [ window orderFront: self ];
    [ window makeKey: self ];
    [ window _setHidden: NO ];

    /* Initialize the CovertFlow layer */
    cfIntLayer = [ [ CAScrollLayer alloc ] initWithBounds:
        CGRectMake(0, 0, rect.size.width, rect.size.height + 128)
    ];
    [ cfIntLayer setDelegate:self ];

    /* Position the CovertFlow layer in the middle of the scroll view */
    cfIntLayer.position = CGPointMake(160, 304);
    [ cfIntLayer setDelegate:self ];

    /* Load the album covers */
    for (i=0; i<j; i++) {
        NSString *filename = [ pictures objectAtIndex: i ];

        background = [ [ [ UIImageView alloc ] initWithFrame:
                CGRectMake(0, 0, COVER_WIDTH_HEIGHT, COVER_WIDTH_HEIGHT)
            ]
            autorelease
        ];
        [ background setImage: [ [ UIImage alloc ] initWithContentsOfFile: filename ] ];
        [ cfIntLayer addSublayer: [ background _layer ] ];
    }

    /* Set the size of the scroll view proportionately to the # covers */
    [ mainView setContentSize:
        CGSizeMake(320, ( (rect.size.height) + (SCROLL_PIXELS*j) ) )
    ];

    /* Add the album layer to the main layer */
    selected = 0;
    [ [ mainView _layer ] addSublayer:cfIntLayer ];
    [ self layoutLayer: cfIntLayer ];
}

- (void) jumpToCover:(int)index
{
    if (index != selected) {
        selected = index;
        [ self layoutLayer:cfIntLayer ];
    }
}


-(void) layoutLayer:(CAScrollLayer *)layer
{
    CALayer *sublayer;
    NSArray *array;
    size_t i, count;
    CGRect rect, cfImageRect;
    NSSize cellSize, spacing, margin;
    CGSize size;
    CATransform leftTransform, rightTransform, sublayerTransform;
    float zCenterPosition, zSidePosition;
    float sideSpacingFactor, rowScaleFactor;
    float angle = 1.39;
    int x;

    size = [ layer bounds ].size;

    zCenterPosition = 60;      /* Z-Position of selected cover */
    zSidePosition = 0;         /* Default Z-Position for other covers */
    sideSpacingFactor = .85;   /* How close should slide covers be */
    rowScaleFactor = .55;      /* Distance between main cover and side covers */

    leftTransform=CATransform3DMakeRotation(angle,-1,0,0);
    rightTransform = CATransform3DMakeRotation(-angle,-1,0,0);

    margin   = NSMakeSize(5.0, 5.0);
    spacing  = NSMakeSize(5.0, 5.0);
    cellSize = NSMakeSize (COVER_WIDTH_HEIGHT, COVER_WIDTH_HEIGHT);

    margin.width += (size.width - cellSize.width * [ pictures count ]
                  -  spacing.width * ([ pictures count ] - 1)) * .5;
    margin.width = floor (margin.width);

    /* Build an array of covers */
    array = [ layer sublayers ];
    count = [ array count ];

    sublayerTransform = CATransform3DIdentity;
    /* Set perspective */
    sublayerTransform.m34 = -0.006;

    /* Begin an CATransaction so that all animations happen simultaneously */
    [ CATransaction begin ];
    [ CATransaction setValue: [ NSNumber numberWithFloat:0.3f ]
        forKey:@"animationDuration" ];

    for (i = 0; i < count; i++)
    {
        sublayer = [ array objectAtIndex:i ];
        x = i;

        rect.size = *(CGSize *)&cellSize;
        rect.origin = CGPointMake(0,0);
        cfImageRect = rect;

        /* Base position */
        rect.origin.x = size.width / 2 - cellSize.width / 2;
        rect.origin.y = margin.height + x * (cellSize.height + spacing.height);

        [ [ sublayer superlayer ] setSublayerTransform: sublayerTransform ];

        if (x < selected)        /* Left side */
        {
            rect.origin.y += cellSize.height * sideSpacingFactor
                           * (float) (selected - x - rowScaleFactor);
            sublayer.zPosition = zSidePosition - 2.0 * (selected - x);
            sublayer.transform = leftTransform;
        }
        else if (x > selected)   /* Right side */
        {
            rect.origin.y -= cellSize.height * sideSpacingFactor
                           * (float) (x - selected - rowScaleFactor);
            sublayer.zPosition = zSidePosition - 2.0 * (x - selected);
            sublayer.transform = rightTransform;
        }
        else                     /* Selected cover */
        {
            sublayer.transform = CATransform3DIdentity;
            sublayer.zPosition = zCenterPosition;

            /* Position in the middle of the scroll layer */
            [ layer scrollToPoint: CGPointMake(0, rect.origin.y
                - (([ layer bounds ].size.height - cellSize.width)/2.0))
            ];

            /* Position the scroll layer in the center of the view */
            layer.position =
                CGPointMake(160.0f, 240.0f + (selected * SCROLL_PIXELS));
        }
        [ sublayer setFrame: rect ];

    }
    [ CATransaction commit ];
}

@end

What’s Going On

  1. When the application instantiates, its applicationDidFinishLaunching method is called by the runtime.

  2. This method first walks through the iPhone’s album directory and takes inventory of all thumbnail images. It then initializes a new window and subclass of the UIScroller view, named CFView, into an object named mainView. Various properties of the scroll view are defined here.

  3. Each photo album thumbnail is loaded into a UIImage object and then assigned to a layer. When the application has finished initializing, the layoutLayer method is called.

  4. The layoutLayer method positions every layer in the view to be oriented according to its position. The selected album is brought to the front, while others are rotated using the rightTransform and leftTransform CATransform objects.

  5. When the user drags his finger, the mouseDragged method is called. This causes a new cover to be selected and causes layoutLayers to be called again.

  6. A CATransaction is created to ensure that all objects are animated at the same time. The newly selected album is oriented to the front of the screen. When the transaction is committed, the Layer Kit framework performs all of the tweening so that it automatically occurs during the specified time interval for the animation (0.03s).

If you enjoyed this excerpt, buy a copy of iPhone Open Application Development.

Copyright © 2009 O'Reilly Media, Inc.