UIKit is the largest iPhone framework in terms of file size, and rightly so—it’s responsible for all user interface functions from creating windows and text boxes to reading multitouch gestures and hardware sensors. All of the graphical pleasantries that make the iPhone seem easy-to-use rely on the UIKit framework to deliver a polished and unified interface. The same UIKit APIs are available to all iPhone applications, and understanding how to use this framework will allow you to take advantage of the same tools that make Apple’s own stock apps spectacular.
UIKit is more than a mere user interface kit; it is also the runtime foundation for iPhone GUI applications. When an application is launched, its main( )
function instantiates a UIApplication
object within UIKit. This class is the base class for all applications having a user interface on the iPhone, and it provides the application access to the iPhone’s higher-level functions. In addition to this, common application-level services such as suspend, resume, and termination are functions of the UIApplication
object.
To tap into the UIKit, your application must be linked to the UIKit framework. As a framework, UIKit is a type of shared library. So, using the compiler tool chain, UIKit can be linked to your application by adding the following arguments to the compiler arguments we described in Chapter 2:
$ arm-apple-darwin-gcc -o MyApp MyApp.m -lobjc
-framework CoreFoundation
-framework Foundation
-framework UIKit
To add this option to the sample makefile from the previous chapter, add the UIKit framework to the linker flags section so that the library is linked in:
LDFLAGS = -lobjc
-framework CoreFoundation
-framework Foundation
-framework UIKit
This chapter is designed to get you up and running with a basic user interface. UIKit supports the following basic user interface elements. The more advanced features of UIKit are covered in Chapter 7.
Windows and views are the base classes for creating any type of user interface. A window represents a geometric space on a screen, while a view acts like a container for other objects. Smaller user interface components, such as navigation bars, buttons, and text boxes are all attached to a view, and that view is anchored to a window. Think of a window as the frame of a painting and the view as the actual canvas. A window can only frame up one view, but views can contain smaller subviews, including other views.
A controlling view is a view that controls how other views are displayed on the screen. The controlling view performs transitions to other views and responds to events occurring on the screen.
Text views are specialized view classes for presenting editor windows to view or edit text or HTML. The Notepad application is a good example of a simple text view. They are considered humble and are rarely used in light of UIKit’s repertoire of more spectacular user interface tools.
The iPhone user interface treats different screens as if they are “pages” in a book. Navigation bars are frequently used to provide a visual prompt to allow the user to return “back” to a previous view, supply them with buttons to modify elements on the current screen page, and provide formatted titles to the screen page they are viewing. Navigation bars are found in nearly all preloaded iPhone applications.
A single screen page is rarely enough for any application to function, especially on a mobile device. Consistent with the spirit of Apple’s user-friendly interfaces, window transitions were introduced with the iPhone to allow the user to perceive navigation through their application like pages in a book. Window transitions are used to make this visual transition from one window to another, and provide various types of different transitions from the familiar page flipping effect to fades and twists.
The equivalent to a pop-up alert window on the iPhone is an alert sheet. Alert sheets appear as modal windows that slide up from the bottom when an operation requires the user’s attention. They are frequently seen on preloaded iPhone applications when a user attempts to delete a number of items or clear important data, such as voicemail. Alert sheets can be programmed to ask the user any question, and present them with any number of different options. They prove useful in parts of an application needing immediate attention.
Tables are really lists that can be used to display files, messages, or other types of collections. They are used for selection of one or more items in a list-like fashion. The table objects are very flexible and allow the developer to define how a table cell should look and behave.
The status bar is the small bar appearing at the top of the iPhone screen, and displays the time, battery life, and signal strength. The status bar’s color, opacity, and orientation can be customized, and the status bar can be made to display icon images to notify the user of a particular application state.
Applications needing to notify the user of time-sensitive information have the ability to display badges on the main applications screen. This alerts the user that the application needs attention, or that the user has messages or other information waiting to be viewed. These are used heavily by applications using the EDGE network to deliver messages.
The status bar is the small bar appearing at the top of the iPhone screen, and displays the time, battery life, and signal strength. Changes can be made to the status bar depending on the application’s needs for style, opacity, and orientation. Images can also be added to the status bar to notify the user of an ongoing operation (such as an alarm or background process).
The most basic user interface component is a window. A window is a region of screen space: a picture frame. UIWindow
is the iPhone’s base window class and is derived from lower level functions that respond to mouse events, gestures, and other types of events that would be relevant to a window. The UIWindow
class is ultimately responsible for holding the contents of a UIView
object (the picture that fits in the window’s frame). UIView
is a base class from which many other types of display classes are derived. The window itself can only hold one object, whereas the UIView
object is designed to accommodate many different types of subobjects, including other views. The two classes go hand-in-hand with each other, and both are required to display anything on the iPhone screen.
Before you can display anything on the iPhone screen, you must create a window to hold content, and to build a window, you need a display region. A display region is a fancy term for rectangle and represents the portion of the screen where the window should be displayed. The underlying structure itself is a rectangle structure named CGRect
that contains two pieces: the coordinates for the upper-left corner of the window and the window’s size (its width and height). Every object displayed on the screen has a display region defining its display area. Most are set when the object is initialized, via an initWithFrame
method. Others must be set after the fact using a ubiquitous method named setFrame
. In the case of the main window, the region’s coordinates are offset to the screen itself; however, all subsequent objects (including the window’s view) are offset to the object that contains it. As other objects are nested inside the view, those objects’ regions will be offset to the view, and so on.
An application uses the entire iPhone screen when it is displayed, and so the window should be assigned a set of coordinates reflecting the view region of the entire screen. This region can be obtained from a static method found inside a class named UIHardware
.
CGRect windowRect = [ UIHardware fullScreenApplicationContentRect ];
This region, named windowRect
above, is then used to create and initialize a new UIWindow
object:
UIWindow *window = [ UIWindow alloc ]; [ window initWithContentRect: windowRect ];
The window frame has now been created, but contains nothing. An object that can render content is needed to place inside of it. A UIView
object must be created to fill the window. Because a window can only hold one view object, the display region for it should likewise use the entire screen so that it will fill up the entire window.
CGRect viewRect = [ UIHardware fullScreenApplicationContentRect ]; viewRect.origin.x = viewRect.origin.y = 0.0; UIView *mainView = [ [ MainView alloc ] initWithFrame: viewRect ];
Before we even get to “Hello, World!”, we need an even more useless application, “Hello, Window!”. This application does nothing more than to create a window and view pair. In fact, because the base UIView
class is just a container class, it can’t even display any text for you. All you’ll see is a black screen. What this application does do is serve as the first few lines of code any GUI application on the iPhone will use.
This application, shown in Example 3-1 and Example 3-2, can be compiled from the tool chain using the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> @interface MyApp : UIApplication { UIWindow *window; UIView *mainView; } - (void)applicationDidFinishLaunching: (NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ autoreleasePool release ]; return returnCode; } @implementation MyApp - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { window = [ [ UIWindow alloc ] initWithContentRect: [ UIHardware fullScreenApplicationContentRect ] ]; CGRect windowRect = [ UIHardware fullScreenApplicationContentRect ]; windowRect.origin.x = windowRect.origin.y = 0.0f; mainView = [ [ UIView alloc ] initWithFrame: windowRect ]; [ window setContentView: mainView ]; [ window orderFront: self ]; [ window makeKey: self ]; [ window _setHidden: NO ]; } @end
The “Hello, Window!” application flows like this:
When the application starts, its main( )
function is called, just as in a regular C program. This hooks into Objective-C land and instantiates an application class named MyApp
, which is derived from UIKit’s UIApplication
class. The main( )
function is also responsible for initializing an auto-release pool. Auto-release pools are used extensively throughout Apple’s Cocoa framework to dispose of objects that have been designated as autorelease
when they are created. This tells the application to simply throw them away when it’s done with them, and they are deallocated later on.
The UIApplication
class’s applicationDidFinishLaunching
method is called by the underlying application object’s framework once the object has initialized. This is where the Objective-C application begins its life.
A call to UIHardware
’s fullScreenApplicationContentRect
method is called to return the coordinates and size of the physical screen. This is then used to create a new window where the application’s main view resides.
The main view is then created, using a display region beginning at 0×0 (the upper-left corner of the window). The view is set as the window’s content.
The window is then instructed to bring itself to the front and show itself. This displays the view, which presently has no content.
The “Hello, Window!” example showed the very minimal code needed to construct and display a window/view pair. Because the UIView
class itself is merely a base class, it didn’t actually display anything. To create a useful application, a new class can be derived from UIView
, allowing its methods to be overridden to add functionality. This controlling view can then display other objects, such as text boxes, images, etc.
To derive a subclass from UIView
, write a new interface and implementation declaring the subclass. The following snippet creates a subclass named MainView
:
@interface MainView : UIView { } - (id)initWithFrame:(CGRect)rect; - (void)dealloc; @end
At the very least, two UIView
class methods should be overridden. The initWithFrame
method is called when the view is first instantiated and is used to initialize the view class. A display region is passed in to define its coordinates (offset to its parent) and size it should display as. Any code that initializes variables or other objects can go into this method. The second method, dealloc
, is called when the UIView
object is disposed of. Any resources previously allocated within your class should be released here.
These two methods are the basis for all other activity within the view class. Here are the templates for them:
@implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { /* Initialize member variables here */ /* Allocate initial resources here */ } return self; } - (void)dealloc { /* Deallocate any resources here */ [ self dealloc ]; [ super dealloc ]; } @end
Now that you know how to derive a UIView
class, you’ve got everything you need to write an application that does something—albeit a mostly useless something. In the tradition of our ancestors, we now present the official useless “Hello, World!” application.
This application, shown in Example 3-3 and Example 3-4, can be built using the same command-line arguments as the previous example:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> #import <UIKit/UITextView.h> @interface MainView : UIView { UITextView *textView; } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching: (NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { textView = [ [ UITextView alloc ] initWithFrame: rect ]; [ textView setTextSize: 18 ]; [ textView setText: @"Hello, World!" ]; [ self addSubview: textView ]; } return self; } - (void)dealloc { [ self dealloc ]; [ super dealloc ]; } @end
The “Hello, World!” example contains everything you’ve seen so far, with the addition of a new view designed to display text:
The application instantiates in the same way as before, by calling the program’s main( )
function, which creates an instance of MyApp
.
Instead of creating a generic UIView
class, the application instantiates its own class called MainView
, which is derived from UIView
.
MainView
’s initWithFrame
method is called, which in turn calls its super class (UIView
) and its own initWithFrame
method to let UIView
do the work of creating the view itself.
A UITextView
, which you’ll learn more about in the upcoming "Text Views" section, is created and attached to the MainView
object. This text view is given the text, "Hello, World!"
.
The window is instructed to display, displaying the MainView
object and the UITextView
object attached to it.
Now that you have something to look at in your application, play around with it for a little while before proceeding.
Try changing the origins and size of the frame passed to mainView
. What happens to the window and its child? How about when changing the display origin of textView
?
Check out the UIWindow.h and UIView.h prototypes in your tool chain’s include directory. You’ll find them in /usr/local/arm-apple-darwin/include/UIKit/.
The UITextView
class is based on a UIView
, however, its functionality has been extended to present and allow editing of text, provide scrolling, and handle various fonts and colors. Text views can be easily abused, and are only recommended for text-based portions of an application, such as an electronic book, notes section of a program, or informational page to present unstructured information.
A UITextView
object inherits from UIScroller
, which is a generic scrollable class. This means that the text view itself comes pre-equipped with all scrolling functionality, so the developer can focus on presenting content rather than programming scroll bars. The UIScroller
class inherits from UIView
, which, as discussed in the last section, is the base class for all view classes.
Because UITextView
is ultimately derived from UIView
, it is created in the same fashion as the main view objects created in the last section—using an initWithFrame
method.
UITextView *textView = [ [ UITextView alloc ] initWithFrame: viewRect ];
Once the view is created, a number of different properties can then be set.
If the text view is being used to collect user input, it will need to be made editable:
[ textView setEditable: YES ];
If the view is simply presenting data that should not be edited, this feature should be disabled.
The size of the top margin is the only margin that can be set in the text view. The value represents the number of pixels from the top of the text view that the text should be offset.
[ textView setMarginTop: 20 ];
The text size, font and color can be set by passing point sizes and font and color objects into the class. These settings work for text and HTML uses, although they serve only as a default for HTML.
The text size is the simplest property, and is passed as an integer measuring point size.
[ textView setTextSize: 12 ];
The font is passed in as a CSS compliant string identifying the font properties.
[ textView setTextFont: @"font-family: Helvetica; font-style: italic; font-weight: bold" ];
The text color is a bit trickier and requires the use of another framework on the iPhone named Core Graphics. To create a simple RGB color, a set of four floating-point values for red, green, blue, and alpha (opacity) are specified with values between 0.0 and 1.0. These represent values ranging from 0% (0.0) to 100% (1.0). The values are used to create a color reference, which is then assigned in the text view as a CGColorRef
(“Core Graphics color reference”).
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); float opaqueRed[4] = {1.0, 0.0, 0.0
,1.0
}; CGColorRef red = CGColorCreate(colorSpace, opaqueRed); [ textView setTextColor: red ];
The Core Graphics framework will be explored more in Chapter 4.
Two different methods can set the content of a text view: setText
and setHTML
. As the name suggests, invoking the setText
method causes the content to be displayed as text, while the setHTML
treats the content like a web page. If you try to call setText
using HTML input, it will appear as HTML source code.
An example of the text display is:
[ textView setText: @"Hello, world!" ];
while HTML can be displayed like this:
[ textView setHTML: @"<b><center>Hello, World!</center></b>" ];
Text views are generally attached to the main view of a window. This allows other subviews, such as navigation bars and controls, to be added to the same parent view later on.
[ mainView addSubview: textView ];
Every iPhone shipped by Apple contains a rather lengthy HTML file containing all of Apple’s legal disclaimers. This is displayed in the legal section of Apple’s Settings application. This example will take this file and display it in a text box.
To compile this application, you’ll need to include the Core Graphics framework, which contains the routines needed to mix colors.
This application, shown in Example 3-5 and Example 3-6, can be built using the tool chain on the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
-framework CoreGraphics
#import <CoreFoundation/CoreFoundation.h> #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import <UIKit/UITextView.h> @interface MainView : UIView { UITextView *textView; } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; @end
#include <stdio.h> #import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { FILE *file; char buffer[262144], buf[1024]; textView = [ [ UITextView alloc ] initWithFrame: rect ]; [ textView setTextSize: 12 ]; file = fopen("/Applications/Preferences.app/English.lproj/ legal-disclaimer-iphone.html", "r"); if (!file) { CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); float opaqueRed[4] = { 1.0, 0.0, 0.0, 1 }; CGColorRef red = CGColorCreate(colorSpace, opaqueRed); [ textView setTextColor: red ]; [ textView setText: @"ERROR: File not found" ]; } else { buffer[0] = 0; while((fgets(buf, sizeof(buf), file))!=NULL) { strlcat(buffer, buf, sizeof(buffer)); } fclose(file); [ textView setHTML: [ [ NSString alloc ] initWithCString: buffer ]]; } [ self addSubview: textView ]; } return self; } - (void)dealloc { [ self dealloc ]; [ super dealloc ]; } @end
The example reads in and displays the contents of a file as follows:
When the application initializes, the main view is created and its initWithFrame
method is called.
The initWithFrame
method instantiates a new UITextView
object and assigns a text size.
The file legal-disclaimer-iphone.html is opened using POSIX C’s fopen
method.
If the file cannot be found, a red color is created and an error message is set as the text of the window.
If the file is found, it is read into a text buffer and then set as the HTML content of the window.
The text view itself is added as a subview to the controlling view where it is displayed to the user.
Here are some productive ways to play with this example:
Copy your favorite web site’s HTML over to the iPhone using SCP. Modify this example to display your file instead. What limitations does the UITextView
appear to have when displaying HTML?
What other font styling information can you pass to the text view’s setTextFont
method? See W3’s CSS specification at http://www.w3.org.
Check out the UITextView.h prototypes in the tool chain’s include directory. You’ll find it in /usr/local/arm-apple-darwin/include/UIKit/.
The iPhone doesn’t support toolbars in the traditional desktop sense. Because each screen of an application is considered a page in a book, Apple has made their version of the toolbar for iPhone to appear more book-like as well. In contrast to toolbars, which can display a clutter of different icons, navigation bars are limited to include a page title (e.g., “Saved Messages”), directional buttons to parent pages, and text buttons for context-sensitive functions such as turning on the speakerphone or clearing a list of items. Navigation bars can also support controls to add tabbed buttons such as the “All” and “Missed” call buttons when viewing recent calls.
To create a navigation bar, instantiate it as an object and call its initWithFrame
method—just like a UIView
.
UINavigationBar *navBar = [ [UINavigationBar alloc] initWithFrame: CGRectMake(0, 0, 320, 48) ];
The above creates a navigation bar 48 pixels high (the default) at position 0x0
(the upper-left corner of its parent view). This is the general convention, but a navigation bar can be created anywhere within the window. By using a different vertical offset, you can place the navigation bar at the bottom of the window or underneath another navigation bar.
To know when something happens on the navigation bar, such as a button press, use the navigation bar’s delegate. A delegate is an object that acts on behalf of another object. By setting the navigation bar’s delegate to self
, you can have it send events such as button presses to the object that created the navigation bar.
[ navBar setDelegate: self ];
Animations are simple fade transitions that occur when transitioning from an old set of buttons to a new set—for example, if a button is changed after it is pressed. Calling enableAnimation
uses these smoother transitions instead of an instantaneous change.
[ navBar enableAnimation ];
Once the navigation bar has been created and initialized, its title and buttons can then be configured. These can be changed even after the navigation bar is displayed; the bar will change to accommodate its new settings.
The navigation title appears as large white text centered in the middle of the bar. The title is frequently used to convey to the end user what sort of information is being displayed, for example, “Saved Messages.”
The title is created as a UINavigationItem
object. This is the base class for anything that natively attaches to a navigation bar, including buttons and other objects. When the title is added to the navigation bar, its UINavigationItem
object is pushed onto it like an object on a stack. Because of this, you’ll want to store a pointer to the title inside the program somewhere. This will let you go back and change the title without having to pull it off the stack first, which would reconfigure the entire navigation bar.
UINavigationItem *navItem = [ [ UINavigationItem alloc ] initWithTitle:@"My Example" ]; [ navBar pushNavigationItem: navItem ];
Here, navItem
points to a newly created UINavigationItem
object and is assigned the title “My Example.” When the user does something that would cause the title to change, navItem
can be changed to something else.
[ navItem setTitle: @"Another Example" ]
Buttons can be added to the left and/or right sides of the navigation bar. Because space is limited, the only buttons that should be added are those for functions specific to the page being displayed. At any time, the navigation bar’s buttons can be changed. With animations enabled, the user will see a smooth, animated transition to the new set of buttons.
Using the example navBar
object created earlier, add two buttons to the navigation bar, one labeled “Good” and one “Evil.”
[ navBar showLeftButton:@"Good" withStyle: 0 rightButton:@"Evil" withStyle: 0 ];
If you need only one button, you can replace the other button’s string with nil
. For example, if you wanted to only provide the user with “Evil” as an option:
[ navBar showLeftButton:nil
withStyle: 0
rightButton:@"Evil" withStyle: 0 ];
In addition to setting button titles, UIKit also allows for different styles. For instance, notice that when you push the Speaker
button in the iPhone’s phone application, the button turns blue. Some buttons also appear to have an arrow shape, conveying the concept of “go back” to the user. By changing the value for the withStyle
parameter in the method call previously shown, you can choose from one of four different button styles. Pay special attention to the style numbers, which are zero-indexed.
Style | Description |
0 | Default style, just a plain gray button |
1 | Colors the button red, useful for warning toggles such as |
2 | Creates the button in the shape of a left (back) arrow |
3 | Colors the button blue, useful for emphasis or general toggles such as |
Apple may add additional styles in future releases of iPhone software, but for now, any other value defers back to the default style.
If you don’t plan on using styles at all, there’s a shortcut method to creating navigation bar buttons that gets rid of some of the work involved:
[ navBar showButtonsWithLeftTitle:@"Back" rightTitle: nil leftBack: YES ];
This method can be used to create a quick set of buttons with no custom styles except for the back arrow button; it is specified by passing YES
for the leftBack
parameter.
The navigation bar itself can be displayed in one of a few different styles. The default style is the standard gray appearance. Three different styles are presently supported.
Style | Description |
0 | Default style, gray gradient background with white text |
1 | Solid black background with white text |
2 | Transparent black background with white text |
The style is set using the call setBarStyle
:
[ navBar setBarStyle: 0 ];
Once you’ve laid out the initial look for your navigation bar, it’s time to display it within your application. Navigation bars are attached to a view object, such as the main view you created in the last example.
[ self addSubview: navBar ];
If you would like to hide the navigation bar, just pull it off the view:
[ navBar removeFromSuperview ];
When you’re finished with the navigation bar entirely, it can be disposed of by releasing it. This can be done in the view class’s dealloc
method:
[ navBar release ];
At this point, your navigation bar is displayed on the screen, but the buttons do nothing. Earlier, the navigation bar’s delegate was set to self
, the calling view. Because the calling view is acting on the navigation bar’s events, it will need the ability to intercept button presses.
To do this, a method named buttonClicked
must be overridden. As you’ll see in the coming chapters, many different types of objects have a buttonClicked
method, but take different arguments. Objective-C supports polymorphism, which allows the developer to support multiple methods sharing the same name, but with different argument types. The runtime will choose the most appropriate version of the method to call.
As the view class has already been set as the delegate for the navigation bar, it will be receiving all button click events that occur on it. This requires that the view class have a method named buttonClicked
accepting the same arguments as if it were the navigation bar itself.
- (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button;
The buttonClicked
method in the view class gets notified as if it belonged to the navigation bar. It is called with the same arguments that the navigation bar’s buttonClicked
method would be called; a pointer to the navigation bar and an integer identifying the button number that was pressed. When using left and right navigation buttons, the value for button
is either a 1 or a 0, signifying the left or right button, respectively.
- (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button { switch(button) { case 1: /* Left button handled here */ break; case 0: /* Right button handled here */ break; } }
If at any point you would like to disable an existing navigation bar button, you have two options. You can make another call to one of the show-button methods (showLeftButton
or showButtonsWithLeftTitle
) using nil
to make the button disappear. Alternatively, if you would like the button to remain visible, but be disabled so that the user can’t press it, use the setButton
method:
[ navBar setButton: 0 enabled: NO ];
Just as in the buttonClicked
method, the button number used here corresponds with either the left (1) or the right (0) button.
Controls are small, self-contained user interface components that can be used by various UIKit classes. They can be glued to many different types of objects, allowing the developer to add additional interaction to a window. One common control found in the navigation bars of Apple’s preloaded applications is the segmented control.
You’ll notice in many preloaded applications that Apple has added buttons to further separate the display of information. For example, the navigation bar in the iTunes WiFi Store application sports “New Releases,” “What’s Hot,” and “Genres” buttons at the top. These further separate the user’s music selection choice. Segmented controls are useful for any such situation where an overabundance of data would best be organized into separate tabs.
An example of setting up such a control with two segments follows:
UISegmentedControl *segCtl = [ [ UISegmentedControl alloc ] initWithFrame:CGRectMake(70.0, 8.0, 180.0, 30.0) withStyle: 2 withItems: NULL ]; [ segCtl insertSegment:0 withTitle:@"All" animated: TRUE ]; [ segCtl insertSegment:1 withTitle:@"Missed" animated: TRUE ]; [ segCtl setDelegate:self ];
The control is then added to the navigation bar in the same way the navigation bar was added to the main view, by adding it as a subview. Only this time, the segmented control is added to the navigation bar. This means the offset of the display region is in relation to the navigation bar, not the enclosing view or the window.
[ navBar addSubview: segCtl ];
Each button in a segmented control is referred to as a segment. The selected segment can be accessed with a call to the selectedSegment
method of the control itself.
int selectedSegment = [ segCtl selectedSegment ];
Chapter 7 offers more on controls.
Someone is designing a mute button to mute their spouse during arguments. In this example application, a navigation bar will be created in an application’s view and assigned a Mute
button. When the button is selected, the button will be changed to red and will also change the title to reflect that the spouse is muted.
Example 3-7 and Example 3-8 can be built using the tool chain on the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> #import <UIKit/UINavigationBar.h> #import <UIKit/UINavigationItem.h> @interface MainView : UIView { UINavigationBar *navBar; /* Our navigation bar */ UINavigationItem *navItem; /* Navigation bar title */ BOOL isMuted; /* Is mute turned on? */ } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; - (UINavigationBar *)createNavBar:(CGRect)rect; - (void)setNavBar; - (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { isMuted = NO; navBar = [ self createNavBar: rect ]; /* Update the navBar for the first time */ [ self setNavBar ]; [ self addSubview: navBar ]; } return self; } - (void)dealloc { [ navBar release ]; [ navItem release ]; [ self dealloc ]; [ super dealloc ]; } - (UINavigationBar *)createNavBar:(CGRect)rect { UINavigationBar *newNav = [ [UINavigationBar alloc] initWithFrame: CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, 48.0) ]; [ newNav setDelegate: self ]; [ newNav enableAnimation ]; /* Add our title */ navItem = [ [UINavigationItem alloc] initWithTitle:@"My Example" ]; [ newNav pushNavigationItem: navItem ]; return newNav; } - (void)setNavBar { if (isMuted == YES) { [ navItem setTitle: @"Spouse (Muted)" ]; [ navBar showLeftButton:nil withStyle: 0 rightButton:@"Mute" withStyle: 1 ]; } else { [ navItem setTitle: @"Spouse" ]; [ navBar showLeftButton:nil withStyle: 0 rightButton:@"Mute" withStyle: 0 ]; } } - (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button { if (button == 0) /* Right button */ { if (isMuted == YES) /* Toggle isMuted */ isMuted = NO; else isMuted = YES; [ self setNavBar ]; /* Update navbar buttons */ } } @end
This example shows you how to accomplish the user-interface portion of this application. You’ll have to write the source code to do the actual spouse muting yourself. Here’s how the example works:
The application instantiates through the main( )
function and returns an instance of the application, just as in previous examples.
A window is created with mainView
as the content. The statement creating the MainView
object also calls its initWithFrame
method. This creates the view and navigation bar, and sets the value for isMuted
to NO
so that the application starts out in an unmuted state.
The initWithFrame
method proceeds to call the setNavBar
method to set up the navigation bar in whatever configuration is presently reflected by the state of isMuted
. The first call sets up the buttons and title for the NO
value.
When the user taps on mute, the buttonClicked
method gets called in the view (the bar’s delegate). This toggles the isMuted
variable and then calls setNavBar
again, which resets the navigation bar’s configuration. This transitions the button to red (button style 1) and sets the title to include Muted
in the text.
If the user taps on the mute button again, the buttonClicked
method is called again, resetting isMuted
back to NO
. It then updates the navigation bar once more with a call to setNavBar
.
With the example snippets from this section and the previous examples, try having a little fun with this example:
Try changing this code to support the Good and Evil buttons example, so that pressing the Good button will cause it to turn blue, and pressing the Evil button will cause it to turn red.
Take the UITextView
code in Example 3-2 and add two buttons, one for HTML and one for Text. Tapping each button should change the text view to display the file in the corresponding format.
Check out the UINavigationBar.h and UINavigationItem.h prototypes in the tool chain’s include directory. These can be found in /usr/local/arm-apple-darwin/include/UIKit/. Experiment with some of the different methods available.
If there’s one thing Apple is well known for, it’s their devotion to aesthetics in their user interfaces. The effect of sliding pages left and right gives the user a sense of the flow of data through an application, or a sense of “forward” and “back.” Even applications lacking a book type of structure can appreciate the smooth slide and fade transitions offered by UIKit. Transition views are objects that allow the current view on the screen to be swapped out smoothly and replaced by another view, with very little programming on the developer’s part.
Transition views inherit from UIView
, so they have most of the properties of a regular view, including a frame. To create a transition, the display region belonging to the view is passed to the transition’s initWithFrame
method.
UITransitionView *transitionView = [ [ UITransitionView alloc ]
initWithFrame: viewRect
];
The same transition view can be used for multiple transition calls and even multiple transition types, so generally only one transition view is needed for a particular display region. If a navigation bar is being used, you must account for its presence by subtracting its height and coordinates from the transition view’s display region:
UITransitionView *transitionView = [ [ UITransitionView alloc ] initWithFrame: CGRectMake(viewRect.origin.x, viewRect.origin.y+ 48.0
, viewRect.size.width, viewRect.size.height −48.0
];
Once the transition has been created, it is added to the view in the same way as other view objects, as a sublayer.
[ self addSubview: transitionView ];
To effect a transition, call the class’s transition
method. You’ll supply the transition style and pointers to the two views between which you are transitioning.
[ transitionView transition: 0 fromView: myOldView toView: myNewView ];
Alternatively, a transition can be called supplying only the new view, not the old one. But be forewarned that this appears to work with only some transitions. Those that do not support this behavior automatically default to using no transition.
[ transitionView transition: 0 toView: myNewView ];
UIKit presently supports 10 distinct transitions. A single transition view can be called repeatedly with different styles, listed in the following table. This allows developers to choose the best transition depending on the particular action at hand, without having to worry about creating a new object for every possible transition they might want to perform.
Style | Description |
0 | No transition/instant transition |
1 | Pages scroll left |
2 | Pages scroll right |
3 | Pages scroll up |
4 | Old page peels up, new page peels down |
5 | Old page peels down, new page peels up |
6 | New page fades over old page |
7 | Pages scroll down |
8 | New page peels up over old page |
9 | New page peels down over old page |
The best example of using page transitions is to illustrate reading a book. In this example, 10 pages of text are created using the UITextView
object covered earlier in this chapter. Using a navigation bar, the user is presented with two buttons to navigate to the previous or next page. Depending on which direction the user has chosen, a different transition is used. When the user reached either end of the book, the corresponding navigation button is disabled to keep them from going any further.
Example 3-9 and Example 3-10 can be compiled using the tool chain on the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> #import <UIKit/UINavigationBar.h> #import <UIKit/UINavigationItem.h> #import <UIKit/UITransitionView.h> #import <UIKit/UITextView.h> #define MAX_PAGES 10 @interface MainView : UIView { UINavigationBar *navBar; /* Our navigation bar */ UINavigationItem *navItem; /* Navigation bar title */ UITransitionView *transView; /* Our transition */ int pageNum; /* Current page number */ /* Some pages to scroll through */ UITextView *textPage[MAX_PAGES]; } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; - (UINavigationBar *)createNavBar:(CGRect)rect; - (void)setNavBar; - (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button; - (void)flipTo:(int)page; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { CGRect viewRect; int i; /* Create a new view port below the navigation bar */ viewRect = CGRectMake(rect.origin.x, rect.origin.y + 48.0, rect.size.width, rect.size.height - 48.0); /* Set our start page */ pageNum = MAX_PAGES / 2; /* Create ten UITextView objects as pages in our book */ for(i=0;i<MAX_PAGES;i++) { textPage[i] = [ [ UITextView alloc ] initWithFrame: rect ]; [ textPage[i] setText: [ [ NSString alloc ] initWithFormat: @"Some text for page %d", i+1 ] ]; } /* Create a navigation bar with 'Prev' and 'Next' buttons */ navBar = [ self createNavBar: rect ]; [ self setNavBar ]; [ self addSubview: navBar ]; /* Create our transition view */ transView = [ [ UITransitionView alloc ] initWithFrame: viewRect ]; [ self addSubview: transView ]; /* Transition to the first page */ [ self flipTo: pageNum ]; } return self; } - (void)dealloc { [ navBar release ]; [ navItem release ]; [ self dealloc ]; [ super dealloc ]; } - (UINavigationBar *)createNavBar:(CGRect)rect { UINavigationBar *newNav = [ [UINavigationBar alloc] initWithFrame: CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, 48.0) ]; [ newNav setDelegate: self ]; [ newNav enableAnimation ]; /* Add our title */ navItem = [ [UINavigationItem alloc] initWithTitle:@"My Example" ]; [ newNav pushNavigationItem: navItem ]; [ newNav showLeftButton:@"Prev" withStyle: 0 rightButton:@"Next" withStyle: 0 ]; return newNav; } - (void)setNavBar { /* Enable or disable our page buttons */ if (pageNum == 1) [ navBar setButton: 1 enabled: NO ]; else [ navBar setButton: 1 enabled: YES ]; if (pageNum == MAX_PAGES) [ navBar setButton: 0 enabled: NO ]; else [ navBar setButton: 0 enabled: YES ]; } - (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button { /* Next Page */ if (button == 0) { [ self flipTo: pageNum+1 ]; } /* Prev Page */ else { [ self flipTo: pageNum−1 ]; } } - (void)flipTo:(int)page { int transitionNum; /* What transition number should be used? */ if (page < pageNum) transitionNum = 2; else if (page > pageNum) transitionNum = 1; else transitionNum = 0; [ transView transition: transitionNum fromView: textPage[pageNum−1] toView: textPage[page−1] ]; pageNum = page; [ self setNavBar ]; } @end
Here’s how the page-flipping example works:
When the application instantiates, a MainView
object is created and its initWithFrame
method is called. This creates ten UITextView
objects to serve as reading page examples, and then creates the navigation bar and one transition object. Finally, an example method named flipTo
is called, which is responsible for flipping to the page number specified.
The flipTo
method decides on a transition style, based on whether the page being flipped to is the next page or the previous one.
After deciding which transition to use, the flipTo
method calls the transition view to do the work of moving to the desired page. It then sets the active page number, which is a variable in the class.
When the user presses the Prev
or Next
navigation buttons, the buttonClicked
method gets called with a pointer to the navigation bar and the button number. From here, the flipTo
method is called again to transition to the new page and disable either navigation button if it has reached one end of the book.
Here are some productive things you can do with the page-flipping example:
Experiment with the different transition styles available and think of actions where you would want to use each transition.
Use the page-flip example and fill in each box with a frame from an ASCII doodle. Now, write a rapid transition across all 10 pages to create an ASCII animation that acts like an old style movie projector.
Check out the UITransitionView.h prototypes in your tool chain’s include directory. You’ll find it in /usr/local/arm-apple-darwin/include/UIKit/.
The iPhone is a relatively small device with limited screen space and no stylus. This means users are going to fumble with their fingers and tap buttons on accident. When this happens, a well-written application prompts the user for confirmation before just blowing away important data. On a desktop computer, applications pop up windows when they need attention. On the iPhone, an alert sheet is slid up from the bottom, graying out the rest of the screen until the user chooses an option. The term “sheet” continues the page metaphor that Apple uses for the iPhone.
The alert sheet is an object that can be instantiated on top of any view, and a single view can host a number of different alert sheets. A basic alert sheet consists of a title, body text, and whatever choices the user should be presented with. It is instantiated in the same way as other UIView
objects, using an initWithFrame
method. Because alert sheets appear at the bottom, the window’s origin can begin halfway down the screen (Y = 240).
UIAlertSheet *alertSheet = [ [ UIAlertSheet alloc ] initWithFrame: CGRectMake(0, 240, 320, 240) ]; [ alertSheet setTitle:@"Computer doesn't like people" ]; [ alertSheet setBodyText: [ NSString stringWithFormat: @"I did not complete your request because I don't like humans." ] ]; [ alertSheet setDelegate: self ];
Alert sheets can stretch their frame to accommodate whatever elements it’s required to hold, so a static 320×240 rectangle should be sufficient for standard alert sheets.
Like navigation bars, alert sheets can be set to support one of three different styles.
Style | Description |
0 | Default style, gray gradient background with white text |
1 | Solid black background with white text |
2 | Transparent black background with white text |
The style can be set with a call to setAlertSheetStyle
.
[ alertSheet setAlertSheetStyle: 0 ];
On rare occasion, it makes sense to display an alert sheet without buttons—for example, to display a progress bar (explained in Chapter 7). In most cases, however, alert sheets are displayed for the purpose of prompting the user. An alert sheet can accommodate as many buttons as can fit on the screen. To add a button, call the alert sheet’s addButtonWithTitle
method:
[ alertSheet addButtonWithTitle:@"OK" ];
Buttons that confirm permanent deletion or some other action that could result in data being destroyed should use what the API refers to as a destructive button. Destructive buttons appear in bright red to alert the user that they are about to perform a significant action that cannot be undone.
The setDestructiveButton
method is used to mark a button as destructive. It accepts a button object as its input, and because addButtonWithTitle
returns a pointer to the new button, the setDestructiveButton
method can be wrapped around it.
[ alertSheet setDestructiveButton: [ alertSheet addButtonWithTitle:@"Confirm Delete" ] ];
Once you’ve set up the text and buttons to be displayed, you can then display the sheet to the user. Five different methods can be called depending on the behavior desired. Each of these methods accepts a class derived as a UIView
, which should be a pointer to whatever view class affected by the alert sheet.
[ alertSheet presentSheetFromAboveView: myView ]; [ alertSheet presentSheetFromBehindView: myView ]; [ alertSheet presentSheetFromButtonBar: buttonBar ]; [ alertSheet presentSheetInView: myView ]; [ alertSheet presentSheetToAboveView: myView ];
The most common way to call an alert sheet is with presentSheetInView
, which can be called from within a view using self
as an argument.
When the alert sheet has been displayed, control returns to the program. As seen earlier in the chapter, many objects use a callback method named buttonClicked
whenever the user taps a button. This allows the application to continue running in the background, to be interrupted only when something actually happens.
The prototype for the alert sheet callback method is below. If the alert sheet’s delegate has been set to the calling view, it is the calling view that will be expected to respond to a button click.
- (void)alertSheet:(UIAlertSheet *)sheet buttonClicked:(int)button;
The buttonClicked
method is called with a pointer to the selected sheet and the index of the button. Buttons are numbered from topmost position to bottom, beginning with 1
(they are not zero-indexed like other values).
- (void)alertSheet:(UIAlertSheet *)sheet buttonClicked:(int)button { if (sheet == alertSheet) { switch(button) { case 1: /* Top-most button was clicked */ break; case 2: /* Second-to-top button was clicked */ break; } } }
Finally, after processing a button press, the alert sheet should vanish—unless, of course, the application has a reason for the user to press more than one button. Use the dismiss
method to make the sheet go away:
[ sheet dismiss ];
The government thought it would be more convenient for the President to carry an iPhone around instead of a suitcase with a big red button. One of their chief programmers conveniently wrote End-of-the-World.app, which the President can press at any time to launch nukes and end the world (or at least start a pie fight). The problem, however, is that he’s almost pressed it accidentally many times. You’ve been contracted to add a confirmation sheet to it—just in case the President didn’t really mean to end the world.
Our example application displays a navigation bar with an End World
button. When pressed, it will first confirm before ending the world as we know it. Polymorphism will be illustrated with the presence of two buttonClicked
methods. When the navigation bar’s button is pressed, the correct buttonClicked
method will be automatically called.
Example 3-11 and Example 3-12 can be built using the tool chain on the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m -lobjc
-framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> #import <UIKit/UIAlertSheet.h> #import <UIKit/UINavigationBar.h> @interface MainView : UIView { UIAlertSheet *endWorldSheet; UIAlertSheet *deniedSheet; UINavigationBar *navBar; } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { /* Create a button to end the world */ navBar = [ [UINavigationBar alloc] initWithFrame: CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, 48.0f) ]; [ navBar setDelegate: self ]; [ navBar enableAnimation ]; [ navBar showLeftButton:nil withStyle: 0 rightButton:@"End World" withStyle: 1 ]; [ self addSubview: navBar ]; } return self; } - (void)alertSheet:(UIAlertSheet *)sheet buttonClicked:(int)button { if (sheet == endWorldSheet) { if (button == 1) { /* Oops, Access Denied */ deniedSheet = [ [ UIAlertSheet alloc ] initWithFrame: CGRectMake(0, 240, 320, 240) ]; [ deniedSheet setTitle: @"Access Denied" ]; [ deniedSheet setBodyText: @"Sorry, you must be super-user to end the world" ]; [ deniedSheet addButtonWithTitle:@"OK" ]; [ deniedSheet setDelegate: self ]; [ deniedSheet presentSheetInView: self ]; } } [ sheet dismiss ]; } - (void)navigationBar:(UINavigationBar *)navbar buttonClicked:(int)button { /* Ask about the end of the world */ endWorldSheet = [ [ UIAlertSheet alloc ] initWithFrame: CGRectMake(0, 240, 320, 240) ]; [ endWorldSheet setTitle: @"Please Confirm" ]; [ endWorldSheet setBodyText:@"I noticed you are trying to end the world. Are you sure you want to do this?" ]; [ endWorldSheet setDestructiveButton: [ endWorldSheet addButtonWithTitle:@"End World" ] ]; [ endWorldSheet addButtonWithTitle:@"Cancel" ]; [ endWorldSheet setDelegate: self ]; [ endWorldSheet presentSheetInView: self ]; } - (void)dealloc { [ self dealloc ]; [ super dealloc ]; } @end
The process flow for this program works in the following fashion.
When the application is instantiates, a MainView
object is created and its initWithFrame
method is called. This creates the view and navigation bar and displays them to the user.
When the user presses the End World
button, the object notifies its delegate’s buttonClicked
method. Because the navigation bar object is expecting to pass a pointer to itself, the buttonClicked
method that accepts a UINavigationBar *
parameter is the one that gets called (based on the rules of polymorphism).
The buttonClicked
method creates an alert sheet called endWorldSheet
and presents it to the user. It immediately returns control, but causes the contents of the view to be dimmed and inaccessible to the user.
When the user presses a button on the alert sheet, the alert sheet object notifies its delegate’s buttonClicked
method. Because the alert sheet class passes a UIAlertSheet *
parameter, its corresponding version of buttonClicked
is called.
The method compares the pointer passed to it with the pointer to endWorldSheet
. If they are the same pointer, the method knows that a button in that sheet was pressed. It then looks at the index number of the button and takes the appropriate action.
If the user confirmed by pressing End World
, the buttonClicked
method itself creates and displays a new alert sheet informing the user that an error has occurred.
When the user presses the OK button, the same buttonClicked
method is called again, only this time, the pointer of the alert sheet passed in will be that of deniedSheet
. The method summarily ignores this and simply dismisses the sheet without taking any further action.
Play around with alert sheets for a bit to get a feel for how they work:
Experiment with adding different buttons to the alert sheet. How many buttons will fit on the screen? How much text can they hold?
Create an alert sheet with no buttons—one that informs the user a file is loading. Use an NSTimer
to wait 10 seconds and then dismiss the sheet without using the buttonClicked
method at all.
Check out the UIAlertSheet.h prototypes in your tool chain’s include directory. You’ll find it in /usr/local/arm-apple-darwin/include/UIKit/. Experiment with some of the less-documented methods.
Tables are the foundation for most types of selectable lists on the iPhone. Voicemail, recent calls, and even email all use the feature-rich UITable
class to display their lists of items. In addition to being a basic list selector, the UITable
class includes built-in functionality to add disclosures, swipe-to-delete, animations, labels, and even images.
A table has three primary components: the table itself, table columns, and table cells (the individual rows in a table). The table’s data is queried from a table’s data binding. A data binding is an interface used by the table to query information about what data to display, such as filenames, email messages, etc. The data source is an object that responds to this query. When the table is created, a data source must be attached to it in order for the table to display anything. It gets called whenever the table is reloaded or new cells are scrolled into view and tells the table which columns and rows to display, along with the data within them.
For most specialized uses, the table can serve as its own data source. This allows the table class and the table’s data to be wrapped cleanly into a single class. To do this, subclass the UITable
object to create a new class for your data. In the following example, a subclass named MyTable
is created. The base class methods used to initialize and destroy the object are overridden to provide the table portion of the class:
@interface MyTable : UITable { } -(id)initWithFrame:(struct CGRect)rect; -(void) dealloc;
To add the data source portion of the class, two methods are used to make the table’s data binding load data: numberOfRowsInTable
and cellForRow
. Because the table is acting as its own data source, you must write these methods into your subclass, where the methods will be responsible for returning column and row data for the table.
- (int)numberOfRowsInTable:(UITable *)_table; - (UITableCell *)table:(UITable *)table cellForRow:(int)row column:(UITableColumn *)col;
We’ll look at these methods later in the section "Data binding.”
When creating a subclass of UITable
, the initialization and destructor methods should be overridden. This enables the subclass to define its own columns and style when it’s created, and properly release any resources it creates for itself.
The initialization method for a UITable
object is initWithFrame
:
- (id)initWithFrame:(struct CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { /* Add additional initialization code here */ } }
Because the table is acting as its own data source, the initWithFrame
method can be used to define the table’s columns. A column is created as a UITableColumn
class and has its own title, identifier, and width inside the table. The UITableColumn
object is also used by many derivative classes such as pickers, discussed in Chapter 7.
UITableColumn *myColumn = [ [ UITableColumn alloc ] initWithTitle: @"Column 1" identifier:@"column1" width: rect.size.width ]; [ self addTableColumn: myColumn ];
To create a self-contained table class, the UITable
object can be designated as its own data source. Issue setDataSource
to bind self
as the data source.
[ self setDataSource: self ];
Override the dealloc
method so you can add code to free any resources that should be disposed of when the object is destroyed. When a MyTable
object is disposed of, the dealloc
method should release its columns and any other resource it has allocated. It will also need to call its superclass’s dealloc
method, to free any resources created internally by the UITable
class.
- (void)dealloc { [ myColumn release ]; [ super dealloc ]; }
The data binding for a UITable
consists of two methods to provide the row data for the table, as mentioned earlier in the section "Subclassing UITable.” The numberOfRowsInTable
method simply returns an integer reflecting the number of rows of data for the table. This value is used by the table object to set the number of rows.
- (int)numberOfRowsInTable:(UITable *)_table { return 3; }
The second method, cellForRow
, returns the individual rows of the table. It is called for every row whenever the row is brought into view. Individual rows are derived from the UITableCell
class. The following example defines a cellForRow
method that creates a cell from the UITableCell
class, sets its title based simply on the row number, and returns the cell.
- (UITableCell *)table:(UITable *)table cellForRow:(int)row column:(UITableColumn *)col { UISimpleTableCell *cell = [ [ UISimpleTableCell alloc ] init ]; NSString *title; title = [ [ NSString alloc ] initWithFormat: @"Row %d", row ]; [ cell setTitle: title ]; return [ cell autorelease ]; }
This simple cell is a text-only cell containing a title, separator, and optional disclosure (described shortly). Because these properties are set for each row cell, any cell in a table can be specially formatted to meet the needs of the data.
Labels are miniature view classes that can be added to table cells to further augment the table cell with decorated text. Different classes of labels exist for different purposes. For instance, a web view label appears as a gray transparent oval containing text. In the following example, a UIWebViewLabel
class is created with offsets that will cause it to appear in the upper-left corner of the cell.
UIWebViewLabel *label = [ [ UIWebViewLabel alloc ] initWithFrame: CGRectMake(0.0, 3.0f, 320.0, 20.0) ]; [ label setText: @"My UIWebView" ]; [ cell addSubview: label ];
The following label classes are available.
Class | Description |
UITextLabel | Displays simple text in its view region |
UITextLabelField | Provides a text entry window where users can enter text |
UIDateLabel | Displays a date in its view region |
UIWebViewLabel | Displays text within a gray transparent surface |
Disclosures are icons appearing at the right side of a table cell to disclose that there is another level of information to be displayed when the cell is selected. These are commonly used on desktop in interfaces such as iTunes, where the user first selects a genre, then artist, and finally, a song.
To enable the disclosure for a particular table cell, use the cell’s setShowDisclosure
method:
[ cell setShowDisclosure: YES ];
The disclosure style can also be changed:
[ cell setDisclosureStyle: 0 ];
Two disclosure styles are available.
Style | Description |
0 | Black arrow |
1 | Blue circle with white arrow |
Tables can display images next to row text. This requires the use of a different type of table cell named UIImageAndTextTableCell
. In the example MyTable
class, this type of object would be returned instead of a UISimpleTableCell
in the cellForRow
method.
UIImageAndTextTableCell *cell = [ [ UIImageAndTextTableCell alloc ] init ]; UIImageView *image = [ [ UIImage alloc ] initWithContentsOfFile: @"/path/to/file.png" ]; [ cell setTitle: @"My row, now with image!" ]; [ cell setImage: image ]; return [ cell autorelease ];
The UIImage
class is used to load an image file. This class is further explained in Chapter 7.
This type of data cell also allows the image and text alignment to be changed using a method called setAlignment
.
[ cell setAlignment: 2 ];
The following alignment styles are supported.
Style | Description |
0 | Image and text left-aligned with margin separator |
1 | Image and text left-aligned with no margin separator |
2 | Image left-aligned, text centered |
3 | Image left-aligned, text right-aligned |
Depending on the size of the images that will be loaded, the row height may need to be changed to accommodate the images. The setRowHeight
method is part of the UITable
base class, and can be set when the table is initialized:
[ self setRowHeight: 64 ];
Because this is a table-level setting, the row height will affect all cells equally.
The UITable
object has built-in logic to intercept swipe gestures and display delete confirmations. This allows the user to swipe his finger across a row he’d like to delete.
To intercept swipe gestures, override the swipe
method in the subclass you derive fom UITable
.
- (int)swipe:(int)type withEvent:(struct _ _GSEvent *)event; { CGPoint point = GSEventGetLocationInWindow(event); CGPoint offset = _startOffset; point.x += offset.x; point.y += offset.y; int row = [ self rowAtPoint:point ]; [ [ self visibleCellForRow: row column:0 ] _showDeleteOrInsertion: YES withDisclosure: NO animated: YES isDelete: YES andRemoveConfirmation: YES ]; return [ super swipe:type withEvent:event ]; }
The swipe
method is passed a gesture event, which is handled by a framework named Graphics Services (covered in Chapter 4). Calling this framework’s GSEventGetLocationInWindow
method will determine the point on the screen where the swipe occurred. Because the point on the screen (and not within the window) is returned, the offset of the window’s position on the screen must be taken into account. For example, if the window appears below a 48-point navigation bar, then the vertical offset must take this into account. In this example, the _startOffset
variable is used to offset screen coordinates to window coordinates.
Once the screen coordinates of the swipe been determined, the row number is then found by handing the coordinates to the rowAtPoint
method belonging to the UITable
class. This returns the row number as an integer.
Next, find the table cell itself using UITable
’s visibleCellForRow
method. This will return a pointer to the selected table cell, whose _showDeleteOrInsertion
method can be invoked to display the delete confirmation. This appears as a red delete button.
To disable the user’s ability to delete rows from the table, this method can just return without taking any action.
When the user confirms that she wants to delete the row (by pressing the delete button), an internal method named _willDeleteRow
is called. When a row is deleted, the row must be removed from the data source; otherwise, it will appear again should the user scroll it off the screen and back. There may also be additional operations the application will need to perform, such as deleting a file or message associated with the cell.
- (void)_willDeleteRow:(int)row forTableCell:(id)cell viaEdge:(int)edge animateOthers:(BOOL)animate { /* Perform any additional deletion operations here */ [ fileList removeObjectAtIndex: row ]; [ super _willDeleteRow: row forTableCell: cell viaEdge: edge animateOthers: animate ]; }
When the user selects a table cell, the table’s tableRowSelected
method is called. This method should be overridden to act on the selection, for example, when opening a file or launching an application. The row number can be accessed using the base class’s selectedRow
method.
- (void)tableRowSelected:(NSNotification *)notification { int index = [ self selectedRow ]; /* A file was selected. Do something here. */ }
Once the row’s index is found, as shown above, you can use it to reference the actual row value from your own data and act on it appropriately. For example, if your table is an array of files, then you would use the index to reference the correct filename in your array. From there, you can load the file or perform whatever other operations your application is designed for.
In this example, we create a custom UITable
class for general use as a file browser. The browser reads a directory supplied by caller, using its setPath
method, and displays all of the files downwind of it matching a file extension also supplied by the caller, using its setExtension
method. The swipe-to-delete functionality has been added for the sake of the example, but (in order to prevent you from trashing files by mistake while experimenting with the example) it deletes the item only from the list, not from the iPhone’s filesystem.
This example can be easily added to any iPhone application to display lists of files by including the following code in an application’s view class:
#import "FileTable.h" FileTable *fileTable = [ [ FileTable alloc ] initWithFrame: rect ]; [ fileTable setPath: @"/Applications" ]; [ fileTable setExtension: @"app" ]; [ fileTable reloadData ]; [ self addSubview: fileTable ];
A complete application, shown in Example 3-13 through Example 3-18, can be compiled from the tool chain using the following command line:
$ arm-apple-darwin-gcc -o MyExample MyExample.m FileTable.m DeletableCell.m
-lobjc -framework CoreFoundation -framework Foundation -framework UIKit
#import <CoreFoundation/CoreFoundation.h> #import <UIKit/UIKit.h> #import "FileTable.h" @interface MainView : UIView { FileTable *fileTable; } - (id)initWithFrame:(CGRect)frame; - (void)dealloc; @end @interface MyApp : UIApplication { UIWindow *window; MainView *mainView; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification; @end
#import "MyExample.h" int main(int argc, char **argv) { NSAutoreleasePool *autoreleasePool = [ [ NSAutoreleasePool alloc ] init ]; int returnCode = UIApplicationMain(argc, argv, [ MyApp class ]); [ 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 ]; } @end @implementation MainView - (id)initWithFrame:(CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { fileTable = [ [ FileTable alloc ] initWithFrame: rect ]; [ fileTable setPath: @"/Applications" ]; [ fileTable setExtension: @"app" ]; [ fileTable reloadData ]; [ self addSubview: fileTable ]; } return self; } - (void)dealloc { [ self dealloc ]; [ super dealloc ]; } @end
#import <UIKit/UIKit.h> #import <UIKit/UISimpleTableCell.h> #import <UIKit/UIImageAndTextTableCell.h> #import <UIKit/UITableColumn.h> #import <UIKit/UIImage.h> #import <GraphicsServices/GraphicsServices.h> @interface FileTable : UITable { NSString *path; NSString *extension; NSMutableArray *fileList; UITableColumn *colFilename; UITableColumn *colType; } - (id)initWithFrame:(struct CGRect)rect; - (void)setPath:(NSString *)_path; - (void)setExtension:(NSString *)_extension; - (void)reloadData; - (int)swipe:(int)type withEvent:(struct _ _GSEvent *)event; - (int)numberOfRowsInTable:(UITable *)_table; - (UITableCell *)table:(UITable *)table cellForRow:(int)row column:(UITableColumn *)col; - (void)_willDeleteRow:(int)row forTableCell:(id)cell viaEdge:(int)edge animateOthers:(BOOL)animate; - (void)dealloc; @end
#import "FileTable.h" #import "DeletableCell.h" @implementation FileTable - (id)initWithFrame:(struct CGRect)rect { if ((self == [ super initWithFrame: rect ]) != nil) { colFilename = [ [ UITableColumn alloc ] initWithTitle: @"Filename" identifier:@"filename" width: rect.size.width - 75 ]; [ self addTableColumn: colFilename ]; colType = [ [ UITableColumn alloc ] initWithTitle: @"Type" identifier:@"type" width: 75 ]; [ self addTableColumn: colType ]; [ self setSeparatorStyle: 1 ]; [ self setDelegate: self ]; [ self setDataSource: self ]; [ self setRowHeight: 64 ]; fileList = [ [ NSMutableArray alloc] init ]; } return self; } - (void) setPath:(NSString *)_path { path = [ _path copy ]; } - (void) setExtension:(NSString *)_extension { extension = [ _extension copy ]; } - (void) reloadData { NSFileManager *fileManager = [ NSFileManager defaultManager ]; NSDirectoryEnumerator *dirEnum; NSString *file; if ([ fileManager fileExistsAtPath: path ] == NO) { return; } [ fileList removeAllObjects ]; dirEnum = [ [ NSFileManager defaultManager ] enumeratorAtPath: path ]; while ((file = [ dirEnum nextObject ])) { if ([ file hasSuffix: extension ] == YES) { [ fileList addObject: file ]; } } [ super reloadData ]; } - (int)numberOfRowsInTable:(UITable *)_table { return [ fileList count ]; } - (UITableCell *)table:(UITable *)table cellForRow:(int)row column:(UITableColumn *)col { if (col == colFilename) { DeletableCell *cell = [ [ DeletableCell alloc ] init ]; [ cell setTable: self ]; UIImageView *image = [ [ UIImage alloc ] initWithContentsOfFile: [ [ NSString alloc ] initWithFormat: @"/Applications/%@/icon.png", [ fileList objectAtIndex: row ] ] ]; [ cell setTitle: [ [ fileList objectAtIndex: row ] stringByDeletingPathExtension ]]; [ cell setImage: image ]; [ cell setShowDisclosure: YES ]; [ cell setDisclosureStyle: 3 ]; return [ cell autorelease ]; } else if (col == colType) { DeletableCell *cell = [ [ DeletableCell alloc ] init ]; [ cell setTable: self ]; [ cell setTitle: extension ]; return [ cell autorelease ]; } } - (int)swipe:(int)type withEvent:(struct _ _GSEvent *)event; { CGPoint point= GSEventGetLocationInWindow(event); CGPoint offset = _startOffset; if (point.x < 100 || point.x > 200) { point.x += offset.x; point.y += offset.y; int row = [ self rowAtPoint:point ]; [ [ self visibleCellForRow: row column: 1 ] _showDeleteOrInsertion: YES withDisclosure: NO animated: YES isDelete: YES andRemoveConfirmation: YES ]; return [ super swipe:type withEvent:event ]; } } - (void)_willDeleteRow:(int)row forTableCell:(id)cell viaEdge:(int)edge animateOthers:(BOOL)animate { [ fileList removeObjectAtIndex: row ]; [ super _willDeleteRow: row forTableCell: cell viaEdge: edge animateOthers: animate ]; } - (void)tableRowSelected:(NSNotification *)notification { NSString *fileName = [ fileList objectAtIndex: [ self selectedRow ] ]; /* A file was selected. Do something here. */ } - (void)dealloc { [ colFilename release ]; [ colType release ]; [ fileList release ]; [ super dealloc ]; } @end
#import "FileTable.h" @interface DeletableCell : UIImageAndTextTableCell { FileTable *table; } - (void)setTable:(FileTable *)_table; @end
#import "DeletableCell.h" @implementation DeletableCell - (void)removeControlWillHideRemoveConfirmation:(id)fp8 { [ self _showDeleteOrInsertion:NO withDisclosure:NO animated:YES isDelete:YES andRemoveConfirmation:YES ]; } - (void)_willBeDeleted { int row = [ table _rowForTableCell: self ]; /* Do something; this row is being deleted */ } - (void)setTable:(FileTable *)_table { table = _table; } @end
The FileTable
class works like this:
When the calling class allocates a new FileTable
object, it calls its initWithFrame
method.
This causes two UITableColumn
objects to be created, which serve as the table’s columns: Filename
and Type
. The class also sets itself as the data source and sets some aesthetic properties.
The file table’s setPath
and setExtension
methods are used to set the desired path and extension for the files to display.
The calling class calls the object’s reloadData
method. This causes the file table to generate a list of files in the directory at the specified path with the extension specified. This list is stored in an array named fileList
. The super class’s reloadData
method is then called, which queries the number of rows from the data source through the numberOfRowsInTable
method.
As the file table is ready to display cells, it calls its data binding again to obtain data for each cell in view. The method’s cellForRow
method is called for each column and row in view. The method returns the appropriate row cells as requested.
If the user swipes across a row, the class’s swipe
method is called. This displays a delete confirmation button by calling the cell’s visibleCellForRow
method with the appropriate properties.
If the user confirms deletion, the table cell’s _willDeleteRow
method is called. This removes the row from the data source so that it will not be redisplayed should the cell be scrolled out of view, then back in.
Try some of these exercises to test your knowledge of tables:
Incorporate this table into the “Hello, World!” example from the beginning of the chapter. Instead of displaying a UITextView
object, build a FileTable
object from this example. Be sure to #import
the class’s header and include it in the source list when compiling.
Modify this example to add a method to enable or disable deletions. This should change the behavior of the swipe
method so that it returns prematurely when deletion is disabled.
Use this example to display applications in the /Applications folder. Modify the example to use the UIImageAndTextTableCell
object to display the application’s icon next to its name.
Check out the UITable.h, UISimpleTableCell.h, UIImageAndTextTableCell.h, and UITableColumn.h prototypes in your tool chain’s directory. These can be found in /usr/local/arm-apple-darwin/UIKit/.
Check out the various types of labels. UITextLabel.h, UIDateLabel.h, UITextFieldLabel.h, and UIWebViewLabel.h prototypes can be found in your tool chain’s directory. These can be found in /usr/local/arm-apple-darwin/UIKit/.
The status bar’s appearance can be customized to meet the look and feel of your application, and can also display notifications about your application. For instance, the iPod application uses the status bar to display a triangular play icon when music is playing in the background. The alarm clock application displays a small clock on the status bar when an alarm is active. Many properties of the status bar can be changed using the UIApplication
and UIHardware
classes.
The status bar mode determines its color, opacity, and orientation. It can also be used to animate the status bar when transitioning between different modes. To set the mode, one of four different setStatusBarMode
methods can be used from within the instance of your application, a UIApplication
object.
- (void)setStatusBarMode:(int)mode duration:(float)duration - (void)setStatusBarMode:(int)mode orientation:(int)orientation duration:(float)duration - (void)setStatusBarMode:(int)mode orientation:(int)orientation duration:(float)duration fenceID:(int)fenceID - (void)setStatusBarMode:(int)mode orientation:(int)orientation duration:(float)duration fenceID:(int)fenceID animation:(int)animation;
The status bar mode sets the overall appearance in color and opacity. The following modes are supported.
Mode | Description |
0 | Default, white status bar |
1 | Black transparent status bar |
2 | Removes status bar image entirely (be sure to also use |
3 | Solid black status bar |
4 | Entirely transparent status bar |
5 | Flashing green status bar with “Touch to return to call” text |
6 | Red transparent status bar |
The orientation determines where the status bar is displayed on the iPhone’s screen. If the iPhone is being held in a landscape fashion, the application may choose to re-orient itself to accommodate the wider display. The value passed with the orientation argument represents the angle at which the iPhone’s screen will be displayed.
Angle | Description |
0 | Status bar is displayed at the natural top of the iPhone |
90 | Status bar is displayed in landscape across the right portion of the screen |
−90 | Status bar is displayed in landscape across the left portion of the screen |
This specifies the number in seconds that an animated transition should take between the previous status bar state and the new one. Use the value 0 for an instantaneous transition.
Used internally. We have no idea what it means.
Sets the animation for transitioning from the previous status bar state to the new one. The following animations are presently supported:
Animation | Description |
0 | Fade out/fade in |
1 | New status bar enters screen from bottom |
2 | Old status bar exits screen from bottom |
3 | Old status bar exits screen from top; new status bar enters from top |
4 | New status bar enters screen from bottom using a different animation |
5 | No animation |
If you’re planning on removing the status bar for a particular purpose, its size must be altered to release the screen space to other views. The status bar size can be set using the UIHardware
class.
To remove the status bar entirely, set its height to zero:
[ UIHardware _setStatusBarHeight: 0.0 ]; [ self setStatusBarMode:2 duration: 0 ];
To restore it:
[ UIHardware _setStatusBarHeight: 20.0 ]; [ self setStatusBarMode:0 duration: 0 ];
Images can be placed on the status bar to notify the user that your application is in a particular state, such as playing music or being logged in to a chat room. Images remain on the status bar even when your application has exited, allowing the user to be notified of future events, such as an alarm. If your application exits prematurely, the image can be automatically removed.
Status bar images are controlled by the SpringBoard application. This makes for a slightly complicated setup in that your application will need to copy the images you use into SpringBoard’s program folder (and restart SpringBoard) before they will work.
Two files are needed to correctly display a status bar image: one for the white status bar and one for the black. Depending on what application is running, the status bar used will automatically switch to the appropriate image.
The first time your application runs, it should copy two files into the SpringBoard folder located at /System/Library/CoreServices/SpringBoard.app. Choose a one-word descriptor for your images and name them Default_NAME.png
and FSO_NAME.png
. The Default
image will be displayed when the white status bar is used, and the FSO
image will be shown on the black status bar.
After these files have been copied into the SpringBoard folder, the SpringBoard program will need to be restarted for them to be recognized. To do this, invoke launchctl
in a shell on the iPhone:
# launchctl stop com.apple.SpringBoard
Alternatively, you can instruct the user to reboot his iPhone. If SpringBoard isn’t restarted, your status bar images will be ignored until the user reboots his iPhone, which may be acceptable.
Once you’ve gotten everything set up, use the addStatusBarImageNamed
method to display the new status bar using the one-word descriptor you came up with before.
[ UIApp addStatusBarImageNamed: @"NAME
" removeOnAbnormalExit: YES ];
The removeOnAbnormalExit
argument instructs the iPhone whether to automatically remove the image if your application crashes.
When you’re ready to remove the image by hand, use the removeStatusBarImageNamed
method:
[ UIApp removeStatusBarImageNamed: @"NAME
" ];
With the iPhone’s numerous different connections—EDGE, WiFi, and Bluetooth (not to mention the cellular network)—lots of things can happen while you’ve got that little device stuck in your pocket. Without some notification to the user that there are pending notifications, they’re likely to miss everything that’s happened while they were busy having a real life. Application badges are small message bubbles that appear on the program’s SpringBoard icon. Application badges are used heavily by Apple’s preloaded applications to alert the user to missed calls, voicemail, text messages, and email.
One of the nice features about these types of badges is that the application doesn’t necessarily need to be running for the badge to display on the SpringBoard. This is useful in serving as a reminder to the user even after they’ve exited the application. This also means you’ll need to clean up any lingering badges when your program exits.
Application badges are one of the easier features to take advantage of, requiring only one call to the UIApplication
class.
[ UIApp setApplicationBadge: @"Hi!" ];
The setApplicationBadge
method takes an NSString
object, which can be built with standard string formatting.
NSString *badgeText = [ [ NSString alloc ] initWithFormat:@"%d", numNewMessages ]; [ UIApp setApplicationBadge: badgeText ];
An application badge should be removed when the user has clicked to the page with the important events they were being notified about. Removing the application badge is also an easy task. A good place to put such code is after transitioning to the view with the events. For example:
[ transitionView transition: 0 toView: missedCalls ]; [ UIApp removeApplicationBadge ];
An application badge will continue to hang around even after an application has terminated. This can be useful, but it’s not always what you want. If an application badge should be removed when the program exits, you’ll also need to make a call to removeApplicationBadge
in the application’s applicationWillTerminate
method.
- (void)applicationWillTerminate { /* We are about to exit, so remove the application badge */ [ UIApp removeApplicationBadge ]; }
You’ll learn more about this kind of application state control in the next section.
Before going on to learn some of the situations in which application badges can help improve your application’s response to state changes, do a little exploration:
Experiment and determine the maximum amount of text that can be added to an application badge. What happens when you exceed this limit?
Check out UIApplication.h in your tool chain’s include directory. You’ll find it in /usr/local/arm-apple-darwin/include/UIKit/. We’ll cover some more UIApplication
methods in Chapter 7.
The state of an application is more important on the iPhone than it is on the desktop. This is because many events on the iPhone can cause an application to suspend, run in the background, or terminate. These different states occur when the user presses the home button, locks the screen, or receives a phone call. It’s important for an application to know when its state is about to change to save any settings, halt threads, or perform other actions.
The UIApplication
base class contains many functions for application state changes, which can be overridden by the application. While there’s nothing the application can generally do about the state it’s about to enter, it can at least take whatever actions are appropriate to prepare for it.
When the home button is pressed, the default behavior for an application is to suspend. It’s also told to suspend during a phone call or when the screen is locked. When this happens, the application’s suspend methods are called.
Depending on the nature of the event, three different suspend methods might be called.
applicationWillSuspendUnderLock
The iPhone screen is locked either by pressing the power button or during an idle screen lock.
applicationWillSuspendForEventsOnly
Events that force the application into the background, such as receiving a phone call, cause this method to be called. Also, if the display is locked, but no applicationWillSuspendUnderLock
method is overridden, this method gets called instead.
applicationSuspend
This method is called when the user presses the home button. It’s also the last chance for an application to perform any necessary actions. If neither of the other two methods have been overridden, this method gets called for all suspend events.
Because one method picks up the slack for others, most applications can service all suspend events by simply overriding applicationSuspend
.
- (void)applicationSuspend:(struct _ _GSEvent *)event { /* We're about to suspend, so do something here */ }
Resources are always a concern with the iPhone, as memory and battery life are finite. If the application doesn’t need to run in the background and there’s no reason to keep the state of the application, it may make sense to just terminate instead of suspending. The iPhone is smart enough to restart the last application being used, so the user might not even know the difference.
- (void)applicationSuspend:(struct _ _GSEvent *)event { [ self terminate ]; }
It may also be appropriate to suspend or terminate depending on the state of the application. For example, if the application is an instant messaging client, it makes sense to suspend only if it’s got a live connection to a server, so that it continue running in the background and maintain the connection. If the user is logged out, there’s no reason to stay alive, and so terminating makes more sense.
- (void)applicationSuspend:(struct _ _GSEvent *)event { if (connected == NO) { [ self terminate ]; }else { /* Set away in IM Client */ } }
When the application is suspended, it’s simply moved to the background and can continue to run. If a separate thread is running to check for new events, such as new instant messages, it is still able to communicate with the process and even send sound events to alert the user (discussed in Chapter 6).
When an application is brought back to a run state, another set of methods are called to prepare the application after it has been brought back to life. These can be overridden to check (and reestablish) connectivity or perform other tasks.
The same three methods used earlier to handle different types of suspends also have complementary resume methods.
applicationDidResumeFromUnderLock
The application resumed from an iPhone whose screen was locked and powered off.
applicationDidResumeForEventsOnly
The application resumed after the end of a phone call or other event that forced it into the background.
applicationDidResume
Catch-all for all other resume events.
The resume methods are overridden in the same way as their suspend counterparts.
- (void)applicationDidResume { /* We've resumed. Do something */ }
Unless an application terminates itself, it’s generally not terminated unless the iPhone is being shut down. Because most good applications do terminate themselves (rather than suspend), it’s a good idea to override the termination method just before an application terminates.
The applicationWillTerminate
method is called whenever an application is about to be cleanly terminated. It is not called when an application is killed by holding down the home button. This method should perform any remaining cleanup of resources, make sure all connections are properly closed, and any other tasks necessary before the program exits.
- (void)applicationWillTerminate { /* We're about to exit, so do something */ }
Here are some other ways you can play with the various application services:
Use a pthread
or NSThread
to create a background thread that maintains an active connection to a server. What happens to the connection when the application suspends?
Check out UIApplication.h in your tool chain’s include directory. You’ll find it in /usr/local/arm-apple-darwin/include/UIKit/. We’ll cover some more UIApplication
methods in Chapter 7.