Chapter     1

Application Recipes

This chapter serves as both a refresher for creating apps for iOS and a foundation for completing the basic tasks that are repeated throughout this book. The first eight recipes walk you through fundamental tasks such as setting up an application, connecting and referencing user interface elements in your code, and adding images and sound files to your project. The knowledge you will acquire from the recipes in this chapter is necessary to complete many of the tasks in the other chapters of this book.

The last four recipes in this chapter deal with select, useful tasks such as setting up simple APIs for default error and exception handling, including a "lite" version of your app in your projects, and making an app launch seem quick and easy in the eyes of the user. For the most part, these tasks won’t be repeated in the remainder of this book; however, we feel they are essential knowledge for any developer.

Recipe 1-1: Setting Up a Single View Application

Many of the recipes in this book are implemented in a test application with a single view. Such a project is easy to set up in Xcode using the Single View Application template. This template enables you to use storyboards.

A storyboard is a way to build interfaces consisting of multiple scenes and their connections. Before the introduction of storyboards in iOS 5, .xib files were typically used to build the interface for individual scenes; in other words, there was one .xib file per scene. The connections between .xib files were handled in code. While you still have the ability to use .xib files, Apple is pushing developers to use storyboards instead. As such, you will be using storyboards for most of this book.

To create a new single view application, go to the main menu and select File arrow.jpg New arrow.jpg Project. This brings up the dialog box with available project templates (see Figure 1-1). The template you want is located on the Application page under the iOS section. Choose “Single View Application” and click “Next.”

9781430259596_Fig01-01.jpg

Figure 1-1. The single view application template in the iOS application section

Enter a few properties for your Application recipes:

  • A Product Name, such as My Test App
  • An Organization Name, which can be your name, unless you already have an organization name
  • A Company Identifier, which preferably is your Internet domain, if you have one

If you like, you can also enter a class prefix that will be applied to all classes you create using the Objective-C file template. This can be a good idea if you want to avoid future name conflicts with third-party code; however, if this app is meant only for testing a feature, you can leave the class prefix item blank.

You also need to specify which device type your application is for: iPad, iPhone, or both (Universal). Choose iPhone or iPad if you are testing. You can also pick Universal, but then the template will generate more code, which you probably don’t need if your only purpose is trying a new feature. Figure 1-2 shows the properties you need to fill out in the project options window.

9781430259596_Fig01-02.jpg

Figure 1-2. Configuring the project

Click the “Next” button and then select a folder where the project will be stored. Bear in mind that Xcode creates a new folder for the project within the folder you pick, so select the root folder for your projects.

There’s often a good reason to place the project under version control. It allows you to check changes to the code so you can go back to a previous version if something goes wrong or if you simply want to see the history of changes to the application. Xcode comes with Git, a feature-rich, open-source version-control system that allows multiple developers to work on a project simultaneously with ease. To initialize it for your project, check the “Create local git repository for this project” checkbox, as in Figure 1-3. As of Xcode 5, you can specify a server as well as your Mac for this repository.

9781430259596_Fig01-03.jpg

Figure 1-3. Selecting the parent folder for the project

Now click the “Create” button. An application with an app delegate, a storyboard, and a view controller class will be generated for you (see Figure 1-4).

9781430259596_Fig01-04.jpg

Figure 1-4. A basic application with an app delegate and a view controller

The setup is now complete, and you can build and run the application (which at this point shows only a blank screen).

At this point, you have a good foundation on which you can build for the next seven recipes.

An Alternate Way

Whether or not to use storyboards is a topic of debate among developers. Some developers love storyboards, while others dislike them. As a developer, it is likely you will work on projects that don’t use storyboards or that use a mix of .xib files and storyboards. We won’t argue for or against storyboards, but many developers tend to agree that storyboards can present difficulties when using version control and when multiple developers need to work on the same storyboard. With this in mind, it might be beneficial to learn the .xib approach. Learning this approach is entirely optional, so you can move on to Recipe 1-2 if you choose.

To create an empty application and add a ViewController class with an accompanying .xib file, you first need to create a new project. Choose “Empty Application” instead of “Single View Application” (refer to Figure 1-1) and then click “Next.”

The next screen will look almost the same as Figure 1-2. This time, there will be a new check box called “Use Core Data.” Leave it cleared and click “Next.” Again, you’ll be prompted for a save location. Find a suitable location to save and click “Create.”

Upon creation of your new, empty application, you’ll see there are no ViewController.m or ViewController.h files; you will need to create those. Click the “+” sign in the lower-left corner of the Xcode window and select “new file” (see Figure 1-5). You can also press Cmd + N.

9781430259596_Fig01-05.jpg

Figure 1-5. Creating a new file in Xcode

Next, you’ll be prompted to choose a template for your new file. Choose “Objective-C class,” as shown in Figure 1-6.

9781430259596_Fig01-06.jpg

Figure 1-6. Creating a new file in Xcode

You will be prompted for file options. Give your new class the name of “ViewController” and choose “UIViewController” from the drop-down box (see Figure 1-7). Make sure you select the “With XIB for user interface” check box. Click “Next.”

9781430259596_Fig01-07.jpg

Figure 1-7. Choosing options for a new file

Now that you have created the class, you will need to select the AppDelegate.m and AppDelegate.h files from the Project Navigator on the left and then modify the code, as shown in Listing 1-1.

Listing 1-1.  Adding properties to the single view AppDelegate.h file

AppDelegate.h

#import <UIKit/UIKit.h>

@class ViewController;
#import "ViewController.h"

@interface AppDelegate : UIResponder<UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;

@end

AppDelegate.m
#import "AppDelegate.h"

@implementation AppDelegate

- (BOOL)Application recipes:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindowalloc] initWithFrame:[[UIScreenmainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewControlleralloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
    [self.windowmakeKeyAndVisible];
returnYES;
}

Once you are finished, you should have a single view application with a single .xib file, as shown in Figure 1-8.

9781430259596_Fig01-08.jpg

Figure 1-8. A single view application with the .xib approach

Recipe 1-2: Linking a Framework

The iOS operating system is organized into frameworks. A framework is a directory of code libraries and resources that are needed to support the library. To use the functionalities of a framework, you need to link the corresponding binary to your project. For UIKit, Foundation, and CoreGraphics frameworks, Xcode does this automatically when you create a new project. However, many important features and functions reside in frameworks such as CoreMotion, CoreData, MapKit, and so on. For these types of frameworks, you need to follow the following steps to add them:

  1. Select the project node (the root node) in the project navigator panel on the left of the Xcode project window (Figure 1-4). This brings up the project editor panel.
  2. Select the target in the Targets dock, as shown on the left of Figure 1-9. If you have more than one target, such as a unit test target, you need to perform these steps for each of them.

    9781430259596_Fig01-09.jpg

    Figure 1-9. Adding the Core Data framework

  3. Navigate to the Build Phases tab and expand the Link Binary with Libraries section. There you will see a list of the currently linked frameworks. Alternatively, you can scroll to the bottom of the page under the General tab.
  4. Click the “Add items (+)” button at the bottom of the list. This brings up a list of available frameworks.
  5. Select the framework you want to link and use the “Add” button to include it (see Figure 1-9).

Tip   To make it easier to find a particular framework, you can use the search field to filter the list.

When you add a framework to your project, a corresponding framework reference node is placed in your project tree (see Figure 1-10).

9781430259596_Fig01-10.jpg

Figure 1-10. When adding a framework, a reference node is created inside the framework group of your process tree

Now, to use the functions and classes from within your code, you only need to import the framework. This is normally done in a header file (.h) within your project, as shown in Listing 1-2, where we import CoreData in the ViewController.h file.

Listing 1-2.  Importing the CoreData framework

//
//  ViewController.h
//  My Single View App
//

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@interface ViewController : UIViewController

@end

Note   If you don’t know the header file for a framework, don’t worry. All framework APIs follow the same pattern, namely #import <FrameworkName/FrameworkName.h>.

With the framework binary linked and the API imported, you can start using its functions and classes in your code.

Recipe 1-3: Adding a User Interface Control View

iOS provides a number of built-in control views, such as buttons, labels, text fields, and so on, that you can use to compose your user interface. Xcode makes designing user interfaces easy with a built-in editor, Interface Builder. Interface Builder is a graphical editor, which allows you to edit both .xib files and storyboards by dragging and dropping components. All you need to do is to drag the controls you want from the object library and position them the way you want in your view. The editor helps you make a pleasing user interface by snapping to standard spaces.

In this recipe, we’ll show you how to add a system button to your view. A system button is a button with default iOS styling. We’ll assume you’ve already created a single view application in which to try this.

To create a new button, select the Main.storyboard file to bring up storyboard. Be sure the Utilities View (the panel on the right) is visible. If it isn’t, select the corresponding button in the toolbar (see Figure 1-11).

9781430259596_Fig01-11.jpg

Figure 1-11. The button to hide or show the Utilities View is located in the upper-right corner of Xcode

Make sure the object library is visible in the Utilities View (lower-right corner of Xcode). Click the “Show the Object Library” button (see Figure 1-12) if it isn’t.

9781430259596_Fig01-12.jpg

Figure 1-12. The object library contains the built-in user interface controls

Now locate the button in the Object Library and drag it onto the view. Blue guidelines will help you center it, as shown in Figure 1-13.

9781430259596_Fig01-13.jpg

Figure 1-13. Dragging a system button from the Object Library

Change the text either by double-clicking the button or by setting the corresponding attribute in the attributes inspector, as shown in Figure 1-14. In the attributes inspector, you can also change other attributes, such as color or font. In previous versions of iOS, a button has a border. The new design philosophy set forth by Apple in iOS 7 embraces borderless buttons.

9781430259596_Fig01-14.jpg

Figure 1-14. Setting the button text in the attributes inspector

You can now build and run your application. Your button shows, but it won’t respond if you tap it. For that, you need to connect it to your code by means of outlets and actions, which are the topics of the next two recipes.

Recipe 1-4: Creating an Outlet

iOS is built on the Model-View-Controller design pattern. One effect of this is that the views are completely separated from the code that operates on the views (the so-called controllers). To reference a view from a view controller, you need to create an outlet in your controller and hook it up with the view. An outlet is an annotated property that connects a storyboard object to a class so you can reference it in code. Connecting an outlet can be accomplished in many different ways, but the simplest is to use Xcode’s assistant editor.

We’ll build on what you did in Recipe 1-3 and create an outlet for the button. Although the referenced view in this example is a button, the steps are the same for any other type of view, be it a label, text field, table view, and so on.

To create an outlet, open the main.storyboard file, select the view controller that contains the button, and click the “Assistant Editor” button in the upper-right corner of Xcode (see Figure 1-15.)

9781430259596_Fig01-15.jpg

Figure 1-15. The center button in the editor group activates the assistant editor

With the assistant editor active, the edit area is split in two, showing Interface Builder on the left and the view controller’s header file on the right. Press and hold the “Ctrl” key while dragging a blue line from the button to the code window. A hint with the text "Insert Outlet, Action, or Outlet Collection" should appear, as shown in Figure 1-16.

9781430259596_Fig01-16.jpg

Figure 1-16. Creating an outlet in the assistant editor using Ctrl-drag

Note   Because an outlet is really only a special kind of an Objective-C property, you need to drag the blue line to somewhere it can be declared in code; that is, somewhere between the @interface and @end declarations.

In the dialog box that appears (shown in Figure 1-17), give the outlet a name. This will be the name of the property that you’ll use to reference the button later from your code, so name it accordingly. Be sure that Connection is set to "Outlet" and that the type is correct (it should be UIButton for system buttons). Also, because you are using ARC (Automatic Reference Counting for memory management) by default, outlets should always use the Weak storage type.

9781430259596_Fig01-17.jpg

Figure 1-17. Configuring an outlet

Note   Although Objective-C properties should generally use the Strong storage type, outlets are an exception. The details are beyond the scope of this book, but briefly the reason has to do with internal memory management; using Weak spares you from writing certain cleanup code that you would otherwise have to write. Throughout this book, we assume that you’re creating your outlets using Weak storage.

Click the “Connect” button. By doing this, Xcode creates a property and hooks it up with the button. Your view controller’s header file should now look similar to Figure 1-18; the little dot next to the property indicates that it is connected to a view in the storyboard or .xib file.

9781430259596_Fig01-18.jpg

Figure 1-18. An outlet property connected to a button in the storyboard

The outlet is now ready, and you can reference the button from your code using the property. To demonstrate that, add the code in Listing 1-3 to the viewDidLoad method in the ViewController.m file.

Listing 1-3.  Demonstrating a referenced outlet

- (void)viewDidLoad
{
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.myButton setTitle:@"Outlet1!" forState:UIControlStateNormal];
}

If you build and run your application, as Figure 1-19 shows, the button’s title should now be “Outlet1!” instead of “Click Me!”

9781430259596_Fig01-19.jpg

Figure 1-19. The button title changed from code using an outlet reference

The next step is to make something happen when the button is tapped. This is what actions are for, which is the topic of the next recipe.

Recipe 1-5: Creating an Action

Actions are the way in which user interface controls notify your code (usually the view controller) that a user event has occurred; for example, when a button has been tapped or a value has been changed. The control responds to such an event by calling the action method you’ve provided.

In this recipe you will continue to build on what you’ve done in Recipes 1-3 and 1-4. The next few pages will help you create and connect an action method to receive “Touch Up Inside” events from the button. You then add code that displays an alert when the user taps the button.

To create an action, your Xcode should still be in assistant-editor mode with both the user interface and the header file showing. If not, press the button shown in Figure 1-15 to make it display.

Ctrl-click and drag the line from the button to the view controller’s @interface section, exactly as you did when you created the outlet earlier. Only this time, change the connection type to Action, as in Figure 1-20.

9781430259596_Fig01-20.jpg

Figure 1-20. Configuring an action method

When you set the connection type to Action, you’ll notice that the dialog box changes to show a different set of attributes. These attributes are different from the outlet connection type. (Compare Figure 1-20 to Figure 1-17.)The new attributes are Type, Event, and Arguments. Usually, the default values provided by Xcode are fine, but there might be situations where you would want to change them. The three attributes can be briefly described as follows:

  • Type: The type of the sender argument; that is, the parameter input type for the action. This can be either the generic type ID or the specific type, which in this case is UIButton. It’s usually a good idea to use the generic type so you can invoke the action method in other situations and not be forced to provide a UIButton (in this case).
  • Event: This is the event type you want the action method to respond to. The most common events are touch events and events that indicate a value has changed. There are numerous kinds of touch events you can choose from.
  • Arguments: This attribute dictates what arguments the action method will have. Possible values are the following:
    • None, which is no argument
    • Sender, which has the type you entered in the Type attribute
    • Sender and Event, which is an object holding additional information about the event that occurred

For the sake of this recipe, leave the attributes at id, Touch Up Inside, and Sender, respectively, but enter showAlert as the name.

Note   The convention in iOS is to name actions according to what will happen when an event gets triggered rather than a name that conveys the event type. Pick names such as showAlert, playCurrentTrack, and shareImage over names such as buttonClicked or textChanged.

Finalize the creation of the action by clicking the “Connect” button in the dialog box. Xcode then creates an action method in the view controller’s class and hooks it up with the button. Your ViewController.h file should now look like Figure 1-21.

9781430259596_Fig01-21.jpg

Figure 1-21. An outlet and an action connected to an object in the storyboard

Now you’re ready to implement the behavior you want when the user taps the button. In this case, you show an alert view that says “Hello Brother!” Add the code in Listing 1-4 to your ViewController.h file.

Listing 1-4.  Implementing the alert behavior

@implementation ViewController

// ...

- (IBAction)showAlert:(id)sender
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Testing Actions"
                                              message:@"Hello Brother!"
                                              delegate:nil
                                              cancelButtonTitle:@"Dismiss"
                                              otherButtonTitles:nil];
    [alert show];
}

@end

You can now build and run the application. When you tap the button, you should see your greeting alert, as in Figure 1-22.

9781430259596_Fig01-22.jpg

Figure 1-22. An action method showing an alert when the button is tapped

Sometimes it happens that the code and the storyboard files get out of sync with connected outlets and actions. Usually this happens when you remove an action method or an outlet property in your code and replace it with a new one. In those cases, you get a runtime error. To fix this, remove the connection from Interface Builder in the connections inspector. The connections inspector can be found in the same pane as the attributes inspector under the circle with the arrow. Figure 1-23 shows two connected action methods for the same event inside the connections inspector. You can remove the lingering action method by clicking the “×” icon next to it.

9781430259596_Fig01-23.jpg

Figure 1-23. A button with two different action methods (showAlert: and sayHello:) connected to the same event

Recipe 1-6: Creating a Class

A common task in iOS programming is to create new classes. Whether your aim is to subclass an existing class or create a new domain-model class to hold your data, you can use the Objective-C class template to generate the necessary files.

In this recipe, we’ll show you how to create and add a new class to your project. If you don’t have a suitable project to try this in, create a new single view application.

For a new class, go to the project navigator and select the group folder in which you want to store the files for your new class. Normally, this is the group folder with the same name as your project, but as your application grows you may want to organize your files into subfolders.

Go to the main menu and select File arrow.jpg New arrow.jpg File (or simply use the keyboard shortcut command [⌘]+ N). Then select the Objective-C class template in the iOS Cocoa Touch section (see Figure 1-24).

9781430259596_Fig01-24.jpg

Figure 1-24. Using the Objective-C class template to create a new class

On the following page, enter “MyClass” in the class field and choose “NSObject” from the subclass of the drop-down list. The convention in Objective-C is to name classes using the PascalCase style.

Here you are creating a new class called “MyClass,” and you are making “NSObject” the parent class. An NSObject class is the best selection for a general class, but you might want to select a parent class of a different type, according to your needs. For example, if you want to create a new view controller, the parent class would be UIViewController.

9781430259596_Fig01-25.jpg

Figure 1-25. Configuring a new class

Note   Depending on which class you select as the parent, you might or might not set additional settings such as Targeted for iPad, or With XIB for user interface. These options are active if you subclass a view controller of some kind.

The next step is to select a physical location on the hard disk and a logical location within your project for your new class; that is, the file folder and the group folder. In this step (see Figure 1-26), you can also decide whether your class should be included in the target (the executable file). This is usually what you want, but there might be situations when you want to exclude files, such as when you have more than one target, like a unit-test target.

9781430259596_Fig01-26.jpg

Figure 1-26. Selecting the physical (file folder) and logical (group folder) places for a class

Most of the time you can just accept the default values for the locations, so go ahead and click “Create.” Xcode then generates two new files for your project: MyClass.h and MyClass.m. They contain the code of an empty class, as in the header and implementation files shown in Listing 1-5 and Listing 1-6.

Listing 1-5.  A  new class header file

//
//  MyClass.h
//  My App
//

#import <Foundation/Foundation.h>

@interface MyClass : NSObject

@end

Listing 1-6.  A new class implementation file

//
//  MyClass.m
//  My App
//

#import "MyClass.h"

@implementation MyClass

@end

Recipe 1-7: Adding an Info.plist Property

The iOS platform uses a special file called Info.plist to store application-wide properties. The file resides in the Supporting Files folder of your project and is named after your project with “-Info.plist” as a suffix. The format of the file is XML, but you can more conveniently edit the values in Xcode’s property list editor, as shown in Figure 1-27.

9781430259596_Fig01-27.jpg

Figure 1-27. The .plist editor in Xcode

The structure of a property list file is that the root element is a dictionary that contains values identified by string keys. The values are often a string but can be other types, such as Booleans, dates, arrays of values, or even dictionaries.

If you select the Info.plist in the project navigator, you see that it already contains several items. These are the most commonly used keys. However, sometimes you need to add a value that isn’t contained by default, such as if your app is using location services and you want to set the NSLocationUsageDescription property.

Follow these steps to add a new application property key and value:

  1. Expand the Supporting Files folder in the project navigator.
  2. Select the file <Application Name>-Info.plist. This brings up the property list editor.
  3. Select the root item, which is called Information Property List.
  4. Press the “Return” key. Xcode adds a new row to the dictionary.
  5. Type the property’s key identifier or select one from the list that is presented to you. Note that if you enter an identifier and Xcode recognizes it as a standard property, it displays a more descriptive key. For example, NSLocationUsageDescription changes into Privacy – Location Usage Description after pressing “Return.” Behind the scenes, though, it’s the identifier you typed in that’s stored.
  6. If the property key isn’t defined within iOS (for example, your own custom key), you are allowed to change the property type. Simply click the type in the Type column and a list of possible values will be displayed.
  7. Enter a value for the key by double-clicking the value column of the new row and typing the new value.

Recipe 1-8: Adding a Resource File

Most apps need to access resource files, such as images or sound files. You enable that by adding them to your project and then referencing them through their names. In this recipe, you add an image file to your project and then use it to populate an image view. Although you use an image file in this example, the process is the same for any other type of file.

As usual, you need a single view project to try this in, so go ahead and create one if you don’t have a suitable one already.

The best way to import a file is to simply drag it from Finder, iPhoto, or any other application that supports the dragging of files. Drag an image file of your liking into the project navigator in Xcode. A good place to put resource files is in the Supporting Files group folder, but you can add it to any group folder within your project.

Note   You can also use the File arrow.jpg Add Files to My App menu item to add resource files to your project.

In the dialog box that appears, be sure to select the “Copy items into destination group’s folder (if needed)” check box, as shown in Figure 1-28.This ensures that your image stays with the project even if you move it to a different location.

9781430259596_Fig01-28.jpg

Figure 1-28. Select the “copy items” check box when adding files

Your image, as Figure 1-29 shows, is now part of your project and can be referenced through its filename.

9781430259596_Fig01-29.jpg

Figure 1-29. An application with an embedded image file

To see how you can reference the image within your application, add an image view to your user interface and make it fill the entire view. Be sure the image view is selected, and then go to the attributes inspector to connect it to your image file by selecting your file from the image attribute’s drop-down menu. You probably should also change the Mode attribute to Aspect Fill or your image might look stretched. If you have been following along since Recipe 1-1, your button created earlier might now be covered up by the image view. To send the image view to the back, select the image view and choose editor arrow.jpg arrange arrow.jpg send to back from the file menu. If necessary, move the button to a lighter area of the photo where it is easier to read.

Your app should now resemble the one in Figure 1-30.

9781430259596_Fig01-30.jpg

Figure 1-30. A user interface with an image view referencing an embedded image file

Using the Asset Catalog

A good way to handle icons and buttons is by using the new Xcode asset catalog feature. The problem with dealing with multiple devices is that screen resolutions might change, as they do for a retina versus non-retina iPhone. For an app to look good on both devices, each piece of artwork needs to have a 1x and 2x resolution copy. The asset catalog organizes these various resolution copies for you, so you need to refer to it by only one name.

In your project navigator, you will see a file called “images.xcassets.” Select this file; you should see an interface in the editor window of Xcode, as shown in Figure 1-31. Here you will see two image sets, one for the app icon and one for the launch image.

9781430259596_Fig01-31.jpg

Figure 1-31. The image assets file seen by selecting image.xcassets from the project navigator

To create an image set, click the “+” button on the lower left of the editor screen and choose “New Image Set,” as shown in Figure 1-32.

9781430259596_Fig01-32.jpg

Figure 1-32. Creating a new image set

Once a new image set is created with a generic name, rename the image set by double-clicking the name. You now will notice that you have a place to put both a 1x and a 2x resolution image into the image set, as shown in Figure 1-33.

9781430259596_Fig01-33.jpg

Figure 1-33. A newly created image set

Add images by simply dragging from your finder into either the 1x or 2x image placeholder box. These images need to be .PNG files or you will not be able to add them to the asset catalog. Select “Done,” and your image set should show the images instead of a placeholder.

Recipe 1-9: Handling Errors

How well your application is handling errors might very well be the difference between success and failure in terms of user experience. Nevertheless, error handling is an oft-neglected part of software development, and iOS apps are no exception. Apps that crash without explanation or fail silently with all sorts of weird behavior are not uncommon.

You should spend a little extra time with handling errors to get them right. Like all things boring, you’re better off dealing with them straight away. One thing you can do to make the effort easier is to create a default error handler—a single piece of code that can handle most of your errors.

This recipe shows you how to set up a simple error handler that, based on the error object you provide, alerts the user and even provides recovery options for the user when available.

Setting Up a Framework for Error Handling

By convention in iOS, fatal, non-recoverable errors are implemented using exceptions. For potentially recoverable errors, however, the course of action is to use a more traditional method with a Boolean return value and an output parameter containing the error details. Listing 1-7 shows an example of this pattern with Core Data’s NSManagedObjectContext:save: method.

Listing 1-7.  A pattern for handling potentially recoverable errors

NSError *error = nil;
if ([managedObjectContext save:&error] == NO)
{
    NSLog(@"Unhandled error: %@, %@", error, [error userInfo]);
}

The error handling in the preceding example dumps the error details to the standard output and then silently carries on as if nothing happened. Obviously, this approach is rather poor and is not the best strategy from a usability perspective.

For this recipe, we’ll build a small framework that helps you handle errors with the same ease as in the code above, but with a much better user experience. When we’re done, your error-handling code will look like Listing 1-8.

Listing 1-8.  A better way of handling potentially recoverable errors with a framework

NSError *error = nil;
if ([managedObjectContext save:&error] == NO)
{
[ErrorHandler handleError:error fatal:NO];
}

Let’s set up the scaffolding for the internal error-handling framework. Start by creating a new single view application project. In this new project, create a new class file that subclasses NSObject. Give the new class the name “ErrorHandler.” Open ErrorHandler.h and add the declaration shown in Listing 1-9.

Listing 1-9.  Adding a declaration for the handleErrors: fatal: class method

//
//  ErrorHandler.h
// Default Error Handling
//

#import <Foundation/Foundation.h>

@interface ErrorHandler : NSObject

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError;

@end

The “+” next to the handleError: fatal: method in Listing 1-9 denotes a class method. This means you do not need to create an instance of a class to call it.

For now, just add an empty stub, a blank method implementation, of this class method in the ErrorHandler.m file. This unfinished method is shown in Listing 1-10. Without this stub, Xcode will give you a warning for incomplete implementation.

Listing 1-10.  Creating a stub for the handleErrors: fatal: class method

//
//  ErrorHandler.m
// Default Error Handling
//

#import "ErrorHandler.h"

@implementation ErrorHandler

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError
{
    // TODO: Handle the error
}

@end

Before implementing the method shown in Listing 1-10, you need to set up code that will test it. You will create a simple user interface with two buttons that fake a non-fatal and a fatal error, respectively. Once completed, you will return and finish this method.

Open Main.storyboard and select the main view. Drag out two buttons from the object library and make them resemble the ones in Figure 1-34.

9781430259596_Fig01-34.jpg

Figure 1-34. Buttons that fake an error and invoke the error-handling method

Create actions for the two buttons, “Fake A Non-Fatal Error” and “Fake A Fatal Error,” as you did in Recipe 1-5. Use the following respective names:

  • fakeNonFatalError
  • fakeFatalError

Make sure you import ErrorHandler.h in the view controller’s header file. ViewController.h should now resemble the code in Listing 1-11.

Listing 1-11.  The ViewController.h file with the import statement and the two actions

//
//  ViewController.h
//  Recipe 1-10: Default Error Handling
//

#import <UIKit/UIKit.h>
#import "ErrorHandler.h"

@interface ViewController : UIViewController

- (IBAction)fakeNonFatalError:(id)sender;
- (IBAction)fakeFatalError:(id)sender;

@end

Now, switch to ViewController.m and add the implementation for the fakeNonFatalError: action method shown in Listing 1-12. It simply creates a fake NSError object with a single recovery option (Retry). The error object is then handed over to the error-handler class, with a flag saying it’s not a fatal error (meaning it shouldn’t cause the app to shut itself down).

Listing 1-12.  Implementing the fakeNonFatalError: action method

- (IBAction)fakeNonFatalError:(id)sender
{
    NSString *description = @"Connection Error";
    NSString *failureReason = @"Can't seem to get a connection.";
    NSArray *recoveryOptions = @[@"Retry"];
    NSString *recoverySuggestion = @"Check your wifi settings and retry.";

    NSDictionary *userInfo =
[NSDictionary dictionaryWithObjects:
@[description, failureReason, recoveryOptions, recoverySuggestion, self]
        forKeys:
@[NSLocalizedDescriptionKey,NSLocalizedFailureReasonErrorKey,
NSLocalizedRecoveryOptionsErrorKey, NSLocalizedRecoverySuggestionErrorKey,
NSRecoveryAttempterErrorKey]];

    NSError *error = [[NSError alloc] initWithDomain:@"NSCookbook.iOS7recipesbook"code:42
userInfo:userInfo];

    [ErrorHandler handleError:error fatal:NO];
}

We won’t explain everything, but the userInfo dictionary that you’ve built for this fake error contains the key NSRecoveryAttempterErrorKey, with the value set to self. What this means is that the view controller acts as the recovery-attempter object for this error. For reasons that will become clear later, you need to implement one method from the NSRecoveryAttempting protocol, namely attemptRecoveryFromError:optionIndex:. This method is invoked on the recovery attempts that you implement later. For the purpose of this recipe, you simply fake a failed recovery attempt by returning NO, as shown in Listing 1-13.

Listing 1-13.  Faking a failed recovery attempt by returning “NO” in the attemptRecoveryFromError method

- (BOOL)attemptRecoveryFromError:(NSError *)error optionIndex:(NSUInteger)recoveryOptionIndex
{
    return NO;
}

Next, implement the fakeFatalError: action method. It creates a simpler error without recovery options, but flags the error as fatal in the error-handling method. The implementation is shown in Listing 1-14.

Listing 1-14.  Implementing the fakeFatalError action method

- (IBAction)fakeFatalError:(id)sender
{
    NSString *description = @"Data Error";
    NSString *failureReason = @"Data is corrupt. The app must shut down.";
    NSString *recoverySuggestion = @"Contact support!";

    NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:
@[description, failureReason,recoverySuggestion]
forKeys:
@[NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey,NSLocalizedRecoverySuggestionErrorKey]];

    NSError *error = [[NSError alloc] initWithDomain:@"NSCookbook.iOS7recipesbook"
                                                code:22 userInfo:userInfo];

    [ErrorHandler handleError:error fatal:YES];
}

Now that you have the interface of the default error-handling API set up and know the ways to test it, you can start implementing the actual error handling.

Notifying the User

The very least a decent default error-handling method should do is notify the user that an error has occurred. Switch back to the ErrorHandler.m file and add the code in Listing 1-15 to the handleError:fatal:method stub you created in Listing 1-10.

Listing 1-15.  Adding a notification to the handleError: fatal method to alert the user of an error

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError
{
    NSString *localizedCancelTitle = NSLocalizedString(@"Dismiss", nil);
    if (fatalError)
        localizedCancelTitle = NSLocalizedString(@"Shut Down", nil);

    // Notify the user
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[error localizedDescription]
                                                    message:[error localizedFailureReason]
                                                   delegate:nil
                                          cancelButtonTitle:localizedCancelTitle
                                          otherButtonTitles:nil];

    [alert show];
    // Log to standard out
    NSLog(@"Unhandled error: %@, %@", error, [error userInfo]);
}

You can now build and run the application to see how this will look. Try tapping the non-fatal error button first, which should display an alert like the one in Figure 1-35.

9781430259596_Fig01-35.jpg

Figure 1-35. An error alert with a fake error

If you tap the button that fakes a fatal error, you’ll get a similar alert. However, for errors flagged as fatal, you want the app to shut down automatically after the user has been notified. Let’s implement that now.

To get started, we’re going to provide the alert view with a delegate. Delegation is a pattern in which one object (the delegate) acts on behalf of another (the delegating object). The object that acts on behalf of another can then send a message back to the delegating object so the delegating object can act accordingly.

With delegation, you can intercept when the user dismisses the alert view. In this way, you can abort the execution there. Add the code to the handleError:fatal: method, as shown in Listing 1-16. At this point your code will not compile, but don’t worry—we will sort it out soon.

Listing 1-16.  Adding a delegate to intercept an alert-view dismissal

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError
{
    NSString *localizedCancelTitle = NSLocalizedString(@"Dismiss", nil);
    if (fatalError)
        localizedCancelTitle = NSLocalizedString(@"Shut Down", nil);

    // Notify the user
    ErrorHandler *delegate = [[ErrorHandler alloc] initWithError:error fatal:fatalError];
    if (!retainedDelegates) {
        retainedDelegates = [[NSMutableArray alloc] init];
    }
    [retainedDelegates addObject:delegate];

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[error localizedDescription]
                                                    message:[error localizedFailureReason]
                                                   delegate:delegate
                                          cancelButtonTitle:localizedCancelTitle
                                          otherButtonTitles:nil];

    [alert show];
    // Log to standard out
    NSLog(@"Unhandled error: %@, %@", error, [error userInfo]);
}

The retainedDelegates hack deserves some explanation. The delegate property of an alert view is a so-called weak reference. What this means is that it will not keep the delegate from deallocating if all other references to it are released.

To keep the delegate from being prematurely deallocated (by ARC), you’ll use a static array. As long as a delegate is a member of the retainedDelegates array, a strong reference is kept that will keep the delegate alive. As you’ll see later, once the delegate’s job is finished it will remove itself from the array, which will allow it to become deallocated.

Add the declaration of the retainedDelegates array at the top of the @implementation block of the ErrorHandler class, as shown in Listing 1-17.

Listing 1-17.  Adding a retainedDelegates declaration to the @implementation block of ErrorHandler.m

//
//  ErrorHandler.m
//  Recipe 1-10 Default Error Handling
//

#import "ErrorHandler.h"

@implementation ErrorHandler

static NSMutableArray *retainedDelegates = nil;

// ...

@end

Now, go to the ErrorHandler.h file again, and add the code shown in Listing 1-18.

Listing 1-18.  Declaring the UIAlertView delegate

//
//  ErrorHandler.h
//  Recipe 1-10: Default Error Handling
//

#import <Foundation/Foundation.h>

@interface ErrorHandler : NSObject <UIAlertViewDelegate>

@property (strong, nonatomic)NSError *error;
@property (nonatomic)BOOL fatalError;

-(id)initWithError:(NSError *)error fatal:(BOOL)fatalError;

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError;

@end

Listing 1-18 turns the ErrorHandler class into an alert-view delegate. Of course, you could have created a new class for this purpose, but this way is a bit easier.

Now, switch to ErrorHandler.m and implement the initWithError: method, as shown in Listing 1-19.

Listing 1-19.  Adding the initWithError: method to the ErrorHandler.m file

-(id)initWithError:(NSError *)error fatal:(BOOL)fatalError
{
    self = [super init];
    if (self) {
        self.error = error;
        self.fatalError = fatalError;
    }
    return self;
}

Finally, implement the clickedButtonAtIndex: delegate method so as to abort in case of a fatal error. As you can see from the code that follows, you also release the delegate by removing it from the retainedDelegates array, as shown in Listing 1-20.

Listing 1-20.  Implementing the clickedButtonAtIndex: delegate method

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (self.fatalError) {
// In case of a fatal error, abort execution
        abort();
}
// Job is finished, release this delegate
    [retainedDelegates removeObject:self];
}

If you rerun the app now and tap the “Fake a Fatal Error” button, the app will abort execution right after you’ve dismissed the error message if you tap the “Shut Down” button (see Figure 1-36).

9781430259596_Fig01-36.jpg

Figure 1-36. An alert view displaying a fake fatal error

Notifying the user and logging the error is the least we should do when handling an unexpected error, so the code you’ve implemented so far is a good candidate for a default error-handling method. However, there is one more feature of the NSError class that you should support: recovery options.

Implementing Recovery Options

NSError offers a way for the notifying method to provide custom recovery options. For example, a method that is called upon to establish a connection of some kind might provide a “Retry” option in case of a timeout failure.

The localizedRecoveryOptions array of an NSObject holds the titles of available recovery options as defined by the caller method. Additionally, the localizedRecoverySuggestion property can be used to give the user an idea of how to handle the error. The option titles as well as the recovery suggestions are suitable for communicating directly with the user, such as in an alert view.

The last piece of the NSError recovery functionality is the recoveryAttempter property. This references an object that conforms to the NSErrorRecoveryAttempting informal protocol, which is what you will use to invoke a particular recovery action.

Let’s integrate this information in the handleError:fatalError: method, as shown in Listing 1-21.

Listing 1-21.  Adding recovery options to the handleError: fatal method

+(void)handleError:(NSError *)error fatal:(BOOL)fatalError
{
    NSString *localizedCancelTitle = NSLocalizedString(@"Dismiss", nil);
    if (fatalError)
        localizedCancelTitle = NSLocalizedString(@"Shut Down", nil);

    // Notify the user
    ErrorHandler *delegate = [[ErrorHandler alloc] initWithError:error fatal:fatalError];
    if (!retainedDelegates) {
        retainedDelegates = [[NSMutableArray alloc] init];
    }
    [retainedDelegates addObject:delegate];

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[error localizedDescription]
                                                    message:[error localizedFailureReason]
                                                   delegate:delegate
                                          cancelButtonTitle:localizedCancelTitle
                                          otherButtonTitles:nil];

    if ([error recoveryAttempter])
    {
        // Append the recovery suggestion to the error message
alert.message = [NSString stringWithFormat:@"%@ %@", alert.message,
error.localizedRecoverySuggestion];
        // Add buttons for the recovery options
        for (NSString * option in error.localizedRecoveryOptions)
        {
            [alert addButtonWithTitle:option];
        }
    }

    [alert show];
    // Log to standard out
    NSLog(@"Unhandled error: %@, %@", error, [error userInfo]);
}

The code added in Listing 1-21 checks to see if there is a recoveryAttempter object. If there is one, it adds recovery suggestions and the accompanying buttons to go with those suggestions.

Implement the actual recovery attempts in the alertView:clickedButtonAtIndex: method by making the changes shown in Listing 1-22.

Listing 1-22.  Handling the alert button clicks

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex != [alertView cancelButtonIndex])
    {
        NSString *buttonTitle = [alertView buttonTitleAtIndex:buttonIndex];
        NSInteger recoveryIndex = [[self.error localizedRecoveryOptions]
                                   indexOfObject:buttonTitle];
        if (recoveryIndex != NSNotFound)
        {
            if ([[self.error recoveryAttempter] attemptRecoveryFromError:self.error
                                                             optionIndex:recoveryIndex] == NO)
            {
                // Redisplay alert since recovery attempt failed
                [ErrorHandler handleError:self.error fatal:self.fatalError];
            }
        }
    }
    else
    {
        // Cancel button clicked

        if (self.fatalError)
        {
            // In case of a fatal error, abort execution
            abort();
        }
    }

    // Job is finished, release this delegate
    [retainedDelegates removeObject:self];
}

In Listing 1-22, you handle the instance in which a user clicks the “Retry” button. If this button is clicked, it will retry the task and redisplay the alert if it fails again. Of course, because you created the error, it will always fail, but in a real application this error might be cleared. For example, in a scenario such as getting a connection, the error might be cleared once the connection is established.

You are now finished with the default error-handling method. To test this last feature, build and run the app and tap the “Fake a Non-Fatal Error” button again. This time the error alert should resemble the one in Figure 1-37.

9781430259596_Fig01-37.jpg

Figure 1-37. An error alert with a “Retry” option

Now you have a convenient default error-handling method that extracts information from an NSError object and does the following:

  1. Alerts the user
  2. If available, provides the user with recovery options
  3. Logs the error to the standard error output

In the next recipe we show you how you can handle exceptions with the same ease.

Recipe 1-10: Handling Exceptions

Exceptions in iOS are by convention reserved for unrecoverable errors. When an exception is raised, it should ultimately result in program termination. For that reason, iOS apps rarely catch exceptions internally, but rather let the default exception-handling method deal with them.

The default exception-handling method catches any uncaught exceptions, writes some debug information to the console, and then terminates the program. Although this is the safe way to deal with iOS exceptions, there is a severe usability problem associated with this approach. Because the console can’t be seen from a real device, the app just disappears in the hands of the user, with no explanation whatsoever.

This recipe shows you how you can make the user experience a little better while keeping a reasonably safe exception-handling approach that will work in most situations.

A Strategy for Handling Exceptions

As a general strategy, terminating the app upon uncaught exceptions is a good solution because it’s safe and will prevent bad things, like data corruption, from happening. However, we’d like to make a couple of improvements to iOS’s default exception handling.

The first thing we want to do is notify the user about the uncaught exception. From a user’s point of view, this is best done at the time of the exception, just before the program terminates. However, the program might then be operating in a hostile environment due to fatal errors, such as out-of-memory or pointer errors. In those situations, complex things like user interface programming should be avoided.

A good compromise is to log the error and alert the user on the next app launch. To accomplish this, you need to intercept uncaught exceptions and set a flag that persists between the two sessions.

Setting Up a Test Application

As you did in the preceding recipe, you will set up a test application to try this out. Start by creating a new single view application. Then add a button to the main view to make it look similar to Figure 1-38.

9781430259596_Fig01-38.jpg

Figure 1-38. A button to throw a fake exception

Add an action for the button called throwFakeException and implement it, as shown in Listing 1-23.

Listing 1-23.  Implementing the throwFakeException: method

- (IBAction)throwFakeException:(id)sender
{
    NSException *e = [[NSException alloc] initWithName:@"FakeException"
reason:@"The developer sucks!" userInfo:[NSDictionary dictionaryWithObject:@"Extra info"
forKey:@"Key"]];
    [e raise];
}

With the test code in place, you can move on to the implementation of the handling of exceptions.

Intercepting Uncaught Exceptions

The way to intercept uncaught exceptions in iOS is to register a handler with the NSSetUncaughtException function. The handler is a void function with an NSException reference as the only parameter, such as the one in Listing 1-24.

Listing 1-24.  An example exception handler

void myExceptionHandler(NSException *exception)
{
    // Handle Exceptions
}

The exception handler you will implement should set a flag that informs the app that an exception occurred on the last run. For that you’ll use NSUserDefaults , which is designed for persisting settings between sessions. Now, in the AppDelegate.m file, add the method shown in Listing 1-25.

Listing 1-25.  Adding a method to set a flag in NSUserDefaults when an error occurred on the last run

void exceptionHandler(NSException *exception)
{
    //Set flag
    NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];

[settings setBool:YES forKey:@"ExceptionOccurredOnLastRun"];
    [settings synchronize];
}

Next, register the exception handler in the Application recipes:didFinishLaunchingWithOptions: method, as shown in Listing 1-26.

Listing 1-26.  Registering an exception

-(BOOL)Application recipes:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSSetUncaughtExceptionHandler(&exceptionHandler);

    // Normal Setup Code
    // ...
}

The next thing you’ll do is add a self-check in Application recipes:didFinishLaunchingWithOptions: to see if the previous session ended in an exception. If that’s the case, your app should reset the flag and notify the user with an alert view. Add this code before the line where you register, as shown in Listing 1-27.

Listing 1-27.  Check if an exception flag is set and handle it with an alert view

- (BOOL)Application recipes:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Default exception handling code
    NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];

    if ([settings boolForKey:@"ExceptionOccurredOnLastRun"])
    {
        // Reset exception occurred flag
        [settings setBool:NO forKey:@"ExceptionOccurredOnLastRunKey"];
        [settings synchronize];

        // Notify the user
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"We're sorry"
message:@"An error occurred on the previous run." delegate:nil
cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
        [alert show];
    }

    NSSetUncaughtExceptionHandler(&exceptionHandler);

    // ...
}

Now you have the basic structure of your improved default-handling method in place. If you run the app now and tap the button to raise the fake exception, your app will terminate. However, if you start it up again, you’ll get an alert, such as the one in Figure 1-39, notifying you of the error that occurred earlier.

9781430259596_Fig01-39.jpg

Figure 1-39. An error message notifying the user of an error that caused the app to close down on the previous run

That’s great, but let’s make this feature a bit more useful.

Reporting Errors

Many uncaught exceptions in iOS apps are pure programming errors—bugs that you can fix if you only get proper information about them. For this reason, iOS creates a crash report when an app has terminated due to an uncaught exception.

However, even though a developer can extract the information about crashes, she needs to connect the device to a computer to do so. If your app is out there in the hands of real users, retrieving the crash report might be impossible or inconvenient at best.

As an alternative, we’d like users to be able to send these error reports directly to us. You can make this possible by adding an “Email Report” button to the error alert.

First you need to store information about the exception so you can retrieve it on the next run if the user decides to send the error report. A good way to do this is to use the natural source of error logging, the stderr stream.

The stderr stream is the channel to which NSLog sends the log messages. By default, this is the console, but it’s possible to redirect the stderr stream to a file by using the freopen function. To do that, add the code in Listing 1-28 to the Application recipes:didFinishLaunchingWithOptions: method.

Listing 1-28.  Redirecting the stderr error stream to a file

- (BOOL)Application recipes:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Default exception handling code
    NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];

    if ([settings boolForKey:@"ExceptionOccurredOnLastRun"])
    {
        // Reset exception occurred flag
        [settings setBool:NO forKey:@"ExceptionOccurredOnLastRunKey"];
        [settings synchronize];

        // Notify the user
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"We're sorry" message:@"An error occurred on the previous run." delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
        [alert show];
    }

    NSSetUncaughtExceptionHandler(&exceptionHandler);

    // Redirect stderr output stream to file
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    NSString *documentsPath = [paths objectAtIndex:0];
    NSString *stderrPath = [documentsPath stringByAppendingPathComponent:@"stderr.log"];

    freopen([stderrPath cStringUsingEncoding:NSASCIIStringEncoding], "w", stderr);

    // ...
}

Listing 1-28 makes it so that all entries to NSLog are written to a file named stderr.log in the app’s documents directory on the device. The file is recreated (thanks to the "w" argument) for each new session, so the file only contains information and error logs from the previous run, which is what you want for the error report.

On uncaught exceptions, iOS writes some basic information about the crash and sends it to stderr (and thus your file). However, two important pieces of information are not logged; namely, the exception’s userInfo dictionary and a symbolized (that is, readable) call stack. Fortunately, you can add that information from your exception-handling function, as shown in Listing 1-29.

Listing 1-29.  Modifying the exception handler to the exception’s userInfo and symbolized call stack to the file

void exceptionHandler(NSException *exception)
{
    NSLog(@"Uncaught exception: %@ Reason: %@ User Info: %@ Call Stack: %@",
          exception.name, exception.reason, exception.userInfo, exception.callStackSymbols);

    //Set flag
    NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];

[settings setBool:YES forKey:@"ExceptionOccurredOnLastRun"];
    [settings synchronize];
}

Now that you have the exception data persisted, you can move on to add the button with which the user can send the information to you.

Adding the Button

To add an “Email Report” button, you must make two small changes to the code that creates the alert view in Listing 1-30.

Listing 1-30.  Adding an “Email Report” button to the alert view

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"We're sorry"
message:@"An error occurred on the previous run." delegate:self
cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
[alert addButtonWithTitle:@"Email  Report"];
[alert show];

You added a button to the alert view and told it to send all events to the AppDelegate object (by declaring self as the delegate). To avoid the compiler warning, you also need to make the AppDelegate an alert-view delegate by declaring the UIAlertViewDelegate protocol. To do that, open AppDelegate.h and make the following change:

@interface AppDelegate : UIResponder <UIApplicationDelegate, UIAlertViewDelegate>

//...

@end

Now you can add the alertView:didDismissWithButtonIndex: alert-view delegate method, which will intercept when a user taps the “Email Report” button. Go back to AppDelegate.m and add the code in Listing 1-31.

Listing 1-31.  Adding a delegate method to intercept an “Email Report” button-pressed event

-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
    {
        //todo: Email a report here
    }
}

Before you write the code for the email report, you need to fix a problem with the current code. Because alert views in iOS are displayed asynchronously, the code for redirecting the stderr output is run before Application recipes:didDismissWithButtonIndex: is invoked. This erases the content of the file prematurely. To fix this, you have to make two changes. First, in Application recipes:didFinishLaunchingWithOptions:, be sure the exception-handling setup code is run only if there has been no exception by adding the code in Listing 1-32.

Listing 1-32.  Adding code to check if there has been no exception before handling setup

- (BOOL)Application recipes:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Default exception handling code
    NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];

    if ([settings boolForKey:@"ExceptionOccurredOnLastRun"])
    {
        // Reset exception occurred flag
        [settings setBool:NO forKey:@"ExceptionOccurredOnLastRunKey"];
        [settings synchronize];

        // Notify the user
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"We're sorry" message:@"An error occurred on the previous run." delegate:self cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
        [alert addButtonWithTitle:@"Email a Report"];
        [alert show];
    }
   else
    {

        NSSetUncaughtExceptionHandler(&exceptionHandler);

        // Redirect stderr output stream to file
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
        NSString *documentsPath = [paths objectAtIndex:0];
        NSString *stderrPath = [documentsPath stringByAppendingPathComponent:@"stderr.log"];

        freopen([stderrPath cStringUsingEncoding:NSASCIIStringEncoding], "w", stderr);
    }

    // ...
}

The second task you need to perform is to add the setup code to alertView:didDismissWithButtonIndex: so that it sets up the exception handling for the case in which there has been an exception. The necessary changes have been shown in Listing 1-33.

Listing 1-33.  Adding code to handle the case when there is an exception

-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *stderrPath = [documentsPath stringByAppendingPathComponent:@"stderr.log"];

    if (buttonIndex == 1)
    {
        //todo: Email a report here
    }

NSSetUncaughtExceptionHandler(&exceptionHandler);

  // Redirect stderr output stream to file
freopen([stderrPath cStringUsingEncoding:NSASCIIStringEncoding], "w", stderr);
}

Now you’re ready for the final step: composing an error-report email.

Emailing the Report

When the user presses the “Email Report” button, you’ll use the MFMailComposeViewController class to handle the email. This class is part of the MessageUI framework, so go ahead and link it to the project. (Details about how to link framework binaries can be found in Recipe 1-2).

Next, you need to import MessageUI.h and MFMailComposeViewController.h to your AppDelegate.h file. Also, because you’ll need to respond to events from the mail view controller, add MFMailComposeViewControllerDelegate to the list of supported protocols. The AppDelegate.h file should now look like the code in Listing 1-34.

Listing 1-34.  Importing frameworks and adding the MFMailComposeViewController delegate

#import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate, UIAlertViewDelegate, MFMailComposeViewControllerDelegate>

// ...

@end

Now you can create and present MFMailComposeViewController with the error report. Add the code in Listing 1-35 to the alertView:didDismissWithButtonIndex: method.

Listing 1-35.  Code necessary for creating and presenting MFMailComposeViewController with the error report

-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    NSString *documentsPath = [paths objectAtIndex:0];
    NSString *stderrPath = [documentsPath stringByAppendingPathComponent:@"stderr.log"];

    if (buttonIndex == 1)
    {
        // Email a report
        MFMailComposeViewController *mailComposer = [[MFMailComposeViewController alloc] init];
        mailComposer.mailComposeDelegate = self;
        [mailComposer setSubject:@"Error Report"];
        [mailComposer setToRecipients:[NSArray arrayWithObject:@"[email protected]"]];
        // Attach log file
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                             NSUserDomainMask, YES);
        NSString *documentsPath = [paths objectAtIndex:0];
        NSString *stderrPath = [documentsPath stringByAppendingPathComponent:@"stderr.log"];

        NSData *data = [NSData dataWithContentsOfFile:stderrPath];

        [mailComposer addAttachmentData:data mimeType:@"Text/XML" fileName:@"stderr.log"];
        UIDevice *device = [UIDevice currentDevice];
        NSString *emailBody =
[NSString stringWithFormat:@"My Model: %@ My OS: %@ My Version: %@",
[device model], [device systemName], [device systemVersion]];
        [mailComposer setMessageBody:emailBody isHTML:NO];
        [self.window.rootViewController presentViewController:mailComposer animated:YES
                                                   completion:nil];
    }

    NSSetUncaughtExceptionHandler(&exceptionHandler);

    // Redirect stderr output stream to file
    freopen([stderrPath cStringUsingEncoding:NSASCIIStringEncoding], "w", stderr);
}

To dismiss the mail compose controller, you also need to respond to the mailComposeController:didFinishWithResult:error: message, which is sent by the controller upon send or cancel events. Add the code in Listing 1-36 to your AppDelegate.m file.

Listing 1-36.  Adding a method to respond to a mailComposeController error message

-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
    [self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
}

Your app now has a good default exception handler that is user friendly, useful, and safe. When you test it now, you’ll see that the alert view has an additional button that allows you to send the report through email. Figure 1-40 shows you an example of this.

9781430259596_Fig01-40.jpg

Figure 1-40. An error alert with an option to send an error report by email

A Final Touch

There is one more thing you might want to consider when implementing this recipe. Because you redirect stderr to a file, no error logging is displayed in the output console. This might be a problem during development when you conduct most of your testing in the simulator. During that time, you probably want the error messages to show in the console rather than being stored in a file.

Fortunately, there is a simple solution. You can use the predefined conditional TARGET_IPHONE_SIMULATOR to allow your exception-handling code to work only when the app is run on a real device. This will allow you to log errors to the console. The code in Listing 1-37 shows how to achieve this.

Listing 1-37.  Adding a conditional to handle exceptions only when running on a real device

- (BOOL)Application recipes:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    #if !TARGET_IPHONE_SIMULATOR
    // Default exception handling code

    // ...

    #endif

returnYES;
}

This concludes our default exception-handling recipe. By having a well-considered strategy for handling errors, you not only save yourself time, but you also give the users the respect they deserve. We hope that with the help of these two recipes you have seen the value of good exception handling, and that it’s really not that difficult to achieve. You’ve probably already seen ways in which these simple examples can be improved. Why not venture into that? After all, the sooner you build these features into your app, the more use you’ll have for them.

Recipe 1-11: Adding a Lite Version

Offering a lite version of your app is a great way to give customers a chance to try your app before buying it. Maintaining two code bases, however, can be quite tiresome and can get out of hand as you incorporate new features into your app. This recipe shows you how you can set up the project to include both versions in one code base.

Adding a Build Target

For this recipe, we have created a new single view application with the name “Recipe 1-11: Adding A Lite Version.”

Select your project file in the project and targets lists and then select the build target for your project, as shown in Figure 1-41.Now press ⌘+D to duplicate the target. You are prompted to “Duplicate Only” or “Duplicate and Transition to iPad,” as shown in Figure 1-42. Click “Duplicate Only” to create a new target that you will use for your lite build. This results in a separate build target with which you can implement a second version.

9781430259596_Fig01-41.jpg

Figure 1-41. Selecting a build target for a project

9781430259596_Fig01-42.jpg

Figure 1-42. Project duplication options

Rename the new target by appending “Lite” to the title, which is done by double-clicking the target name. You also should change the new target’s Product Name attribute to signal that it’s a lite version. You’ll find the Product Name attribute in the Build Settings tab under the Packaging heading (see Figure 1-43).

9781430259596_Fig01-43.jpg

Figure 1-43. Appending “Lite” to the target’s name and the Product Name build setting

Tip   The easiest way to find a particular build attribute is to use the search field in the upper-left corner of the Build Settings tab. Just type Product Name and the attributes are filtered out as you type.

You can now build and run the lite version. To do this, you need to change the active scheme. When you duplicated the regular target earlier, Xcode created a new build scheme for you. This new scheme is named identically to the initial name of the duplicated target, namely “Recipe1-11: Adding a Lite Version copy.” Even though you have changed the name of the target to “Recipe 1-11: Adding a Lite Version–Lite”, Xcode doesn’t change the scheme name accordingly. If this bothers you, you can rename it in the Manage Schemes window, which you can reach from the “Active Scheme” button located just right of the stop button (see Figure 1-44). To rename it, highlight the copy and click the name once.

9781430259596_Fig01-44.jpg

Figure 1-44. Go to Manage Schemes if you want to change the name of the lite build scheme

Note   Keep in mind that the two targets must have separate bundle identifiers to show up as separate apps when you install and run them on your device (or simulator). Xcode sets up different bundle identifiers by default, but be careful when making changes.

Coding for a Specific Version

Now you need a way to differentiate between the two builds in your source code. For instance, you might want to limit some features in the lite version or show ads to the non-paying users. Some code in the lite version should not be compiled into the full version and vice versa. This is where preprocessor macros come in handy.

What you want to do is add a preprocessing macro named LITE_VERSION to the lite version target. Again, this is done in the Build Settings tab, under the compiler’s preprocessing header. You’ll need to expand the Build Settings tab to view all build settings, done by pressing the “All” button in the top-left corner of the window. Hover over the Debug macro, click the add icon (+) that appears next to it, and enter LITE_VERSION in the edit field. Be sure to do the same for the release macro as well. Figure 1-45 shows an example of these changes.

9781430259596_Fig01-45.jpg

Figure 1-45. Defining the LITE_VERSION conditional in the preprocessor macros

To build different features into your app, you will need to use the preprocessor macro you created. Anywhere in your code that you want to specify different code for your lite version versus the full version, use the following #ifdef directive:

#ifdef LITE_VERSION
// Stuff for Lite version
#else
// Stuff for Full version
#endif

Alternatively, you can use the negated form if that’s more convenient:

#ifndef LITE_VERSION
// Code exclusively for Full version
#endif

Note   You can also control which files are included in each build. For example, you might not need to include the full version artwork in the lite version. Click your lite project target and go to the Build Phases tab. Expand the Copy Bundle Resources ribbon and remove or add any files that are specific to the lite version.

Recipe 1-12: Adding Launch Images

For the sake of the user experience, it is generally considered bad practice to show a so-called “splash screen” at application startup. The user is normally eager to start playing with your app and is not interested in your branding information. Nor are they particularly eager to watch your cool video, regardless of how much time and money you spent on it.

The extra few seconds it takes for the user to view and/or dismiss the splash screen can be quite annoying and might be unfavorable to the overall experience.

There is an exception to the rule, however. Many apps take a little while to load, a few seconds during which it remains unresponsive and uninteresting. Because this could be as bad as a splash screen in terms of user experience, your app should do what it can to mitigate the effect. One way of doing this is to display an image that is removed as soon as the launch is finished and the app is ready to take on user actions.

These images are called launch images and are really easy to implement in iOS. The function of displaying an image while the app is loading is already built-in. All you have to do is to provide the appropriate images.

Designing Launch Images

Apple’s philosophy regarding launch images is that they should resemble the main view of the app as much as possible. This makes the loading time seem a little quicker and therefore more responsive. This is a better alternative to an image with a branded, splash-like design.

For example, if your app’s main view consists of a table view, your launch image could look something like Figure 1-46.

9781430259596_Fig01-46.jpg

Figure 1-46. A launch image should resemble the main view of your app

Also, it’s recommended that launch images don’t contain elements that might need to be translated or changed during localization. For example, the launch image in Figure 1-46 doesn’t contain any navigation bar buttons. These have been edited out, as have the elements in the status bar.

Even though there is support for language-specific versions of the launch images, it’s suggested you don’t use it due to the increased amount of space the additional launch images will require.

Launch Image Files

Depending on the nature of your app and which devices it supports, you’ll need to create one or more launch images. Including a launch image in your app is just a matter of adding it to the project. In the past, simple naming conventions told iOS how to categorize images and their appropriate sizes depending on which device is used.

A launch image can be created by first selecting the Images.xcassets file, which should be included in a new single view application by default.

You’ll see that two image sets have been created already. One set is for the app icon and the other is for the launch image. Select the “LaunchImage” image set, and you will see the interface shown in Figure 1-47.

9781430259596_Fig01-47.jpg

Figure 1-47. Asset catalog image-set view

If you click one of these image placeholder squares, you will see the type of image expected in the attributes inspector, as well as which versions of iOS you are supporting. For our 2x image, you’ll see that it is expecting a size of 640 x 960 pixels, as shown in Figure 1-48.

9781430259596_Fig01-48.jpg

Figure 1-48. Asset catalog attributes inspector view, which shows expected size

Once you have created an image of the expected size, you can add it to the project by simply dragging and dropping it onto the image placeholder box shown in Figure 1-49. Just be aware that your launch image has to be of type .PNG or it will not work. Also, if you try to add an incorrectly sized image to the box, you’ll get a compiler warning.

9781430259596_Fig01-49.jpg

Figure 1-49. Asset catalog image drag and drop

After dragging and dropping, the image gets saved to the images.xcassets arrow.jpg LaunchImage.launchimage folder. You will also need to create an image for the R4 image space in Figure 1-49, just as you did for the 2x image.

If you happen to have multiple launch images, you can choose which launch-image set you would like to use by selecting the root project node from the project navigator and changing the launch-image source, which is located under the General tab in the editor window, as shown in Figure 1-50.

9781430259596_Fig01-50.jpg

Figure 1-50. Selecting an image set to use for the launch image

Summary

In this chapter, you’ve acquired (or refreshed) some fundamental knowledge you need to follow the remaining recipes within this book. You’ve seen how to set up a basic application, how to build user interfaces, and how to control them with your code. Additionally, you’ve learned the difference between errors and exceptions in iOS, and you’ve seen how you can implement a strategy to handle them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset