iOS Programming: The Big Nerd Ranch Guide (2014)

18. Saving, Loading, and Application States

There are many ways to save and load data in an iOS application. This chapter will take you through some of the most common mechanisms as well as the concepts you need for writing to or reading from the filesystem on iOS.

Archiving

Most iOS applications are really doing one thing: providing an interface for a user to manipulate data. Every object in an application has a role in this process. Model objects, as you know, are responsible for holding on to the data that the user manipulates. View objects simply reflect that data, and controllers are responsible for keeping the views and the model objects in sync. Therefore, when talking about saving and loading data, we are almost always talking about saving and loading model objects.

In Homepwner, the model objects that a user manipulates are instances of BNRItem. Homepwner would actually be a useful application if instances of BNRItem persisted between runs of the application, and in this chapter, you will use archiving to save and load BNRItem objects.

Archiving is one of the most common ways of persisting model objects on iOS. Archiving an object involves recording all of its properties and saving them to the filesystem. Unarchiving recreates the object from that data.

Classes whose instances need to be archived and unarchived must conform to the NSCoding protocol and implement its two required methods, encodeWithCoder: and initWithCoder:.

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;

- (instancetype)initWithCoder:(NSCoder *)aDecoder;

@end

Make BNRItem conform to NSCoding. Open Homepwner.xcodeproj and add this protocol declaration in BNRItem.h.

@interface BNRItem : NSObject <NSCoding>

Now you need to implement the required methods. Let’s start with encodeWithCoder:. When a BNRItem is sent the message encodeWithCoder:, it will encode all of its properties into the NSCoder object that is passed as an argument. While saving, you will use NSCoder to write out a stream of data. That stream will be stored on the filesystem. This stream is organized as key-value pairs.

In BNRItem.m, implement encodeWithCoder: to add the names and values of the item’s properties to the stream.

- (void)encodeWithCoder:(NSCoder *)aCoder

{

    [aCoder encodeObject:self.itemName forKey:@"itemName"];

    [aCoder encodeObject:self.serialNumber forKey:@"serialNumber"];

    [aCoder encodeObject:self.dateCreated forKey:@"dateCreated"];

    [aCoder encodeObject:self.itemKey forKey:@"itemKey"];

    [aCoder encodeInt:self.valueInDollars forKey:@"valueInDollars"];

}

Notice that pointers to objects are encoded with encodeObject:forKey:, but valueInDollars is encoded with encodeInt:forKey:. Check the documentation for NSCoder to see all of the types you can encode. Regardless of the type of the encoded value, there is always a key, which is a string that identifies which instance variable is being encoded. By convention, this key is the name of the property being encoded.

When an object is encoded (that is, it is the first argument in encodeObject:forKey:), that object is sent encodeWithCoder:. During the execution of its encodeWithCoder: method, it encodes its object instance variables using encodeObject:forKey: (Figure 18.1). Thus, encoding an object is a recursive process where each object encodes its “friends”, and they encode their friends, and so on.

Figure 18.1  Encoding an object

Encoding an object

To be encoded, these objects must also conform to NSCoding. Let’s confirm this for NSString and NSDate. The protocols that a class conforms to are listed in its class reference. Instead of opening up the documentation browser as you have done before, you can take a shortcut to get to the reference directly from your code.

In BNRItem.m, hold down the Option key, mouse over an occurrence of NSString in your code, and click. A pop-up window will appear with a brief description of the class and links to its header file and its reference.

Figure 18.2  Option-clicking NSString

Option-clicking NSString

Click the link to be taken to the NSString class reference, and at the top of the reference you will see a list of protocols that the class conforms to. NSCoding is not in this list, but if you click the NSSecureCoding protocol, you will see that this protocol conforms to NSCoding. Thus, NSString does, as well.

You can do the same check for the NSDate class or take our word for it that NSDate is also NSCoding compliant.

Option-clicking is not just for classes. You can use the same shortcut for methods, types, protocols, and more. Keep this in mind as you run across items in your code that you want to know more about.

Now back to keys and encoding. The purpose of the key used when encoding is to retrieve the encoded value when this BNRItem is loaded from the filesystem later. Objects being loaded from an archive are sent the message initWithCoder:. This method should grab all of the objects that were encoded in encodeWithCoder: and assign them to the appropriate instance variable.

In BNRItem.m, implement initWithCoder:.

- (instancetype)initWithCoder:(NSCoder *)aDecoder

{

    self = [super init];

    if (self) {

        _itemName = [aDecoder decodeObjectForKey:@"itemName"];

        _serialNumber = [aDecoder decodeObjectForKey:@"serialNumber"];

        _dateCreated = [aDecoder decodeObjectForKey:@"dateCreated"];

        _itemKey = [aDecoder decodeObjectForKey:@"itemKey"];

        _valueInDollars = [aDecoder decodeIntForKey:@"valueInDollars"];

    }

    return self;

}

Notice that this method has an NSCoder argument, too. In initWithCoder:, the NSCoder is full of data to be consumed by the BNRItem being initialized. Also notice that you sent decodeObjectForKey: to the container to get objects back and decodeIntForKey: to get the valueInDollars.

In Chapter 2, we talked about the initializer chain and designated initializers. The initWithCoder: method is not part of this design pattern; you will keep BNRItem’s designated initializer the same, and initWithCoder: will not call it.

(By the way, archiving is how XIB files are created. UIView conforms to NSCoding. Instances of UIView are created when you drag them onto the canvas area. When the XIB file is saved, these views are archived into the XIB file. When your application launches, it unarchives the views from the XIB file. There are some minor differences between a XIB file and a standard archive, but overall it is the same process.)

Instances of BNRItem are now NSCoding compliant and can be saved to and loaded from the filesystem using archiving. You can build the application to make sure there are no syntax errors, but you still need a way to kick off the saving and loading. You also need a place on the filesystem to store the saved items.

Application Sandbox

Every iOS application has its own application sandbox. An application sandbox is a directory on the filesystem that is barricaded from the rest of the filesystem. Your application must stay in its sandbox, and no other application can access your sandbox.

Figure 18.3  Application sandbox

Application sandbox

The application sandbox contains a number of directories:

application bundle

This directory contains the executable and all application resources like NIB files and images. It is read-only.

Documents/

This directory is where you write data that the application generates during runtime and that you want to persist between runs of the application. It is backed up when the device is synchronized with iTunes or iCloud. If something goes wrong with the device, files in this directory can be restored from iTunes or iCloud. For example, in Homepwner, the file that holds the data for all your possessions will be stored here.

Library/Caches/

This directory is where you write data that the application generates during runtime and that you want to persist between runs of the application. However, unlike the Documents directory, it does not get backed up when the device is synchronized with iTunes or iCloud. A major reason for not backing up cached data is that the data can be very large and extend the time it takes to synchronize your device. Data stored somewhere else – like a web server – can be placed in this directory. If the user needs to restore the device, this data can be downloaded from the web server again.

Library/Preferences/

This directory is where any preferences are stored and where the Settings application looks for application preferences. Library/Preferences is handled automatically by the class NSUserDefaults (which you will learn about in Chapter 26) and is backed up when the device is synchronized with iTunes or iCloud.

tmp/

This directory is where you write data that you will use temporarily during an application’s runtime. The operating system may purge files in this directory when your application is not running. However, to be tidy you should still explicitly remove files from this directory when you no longer need them. This directory does not get backed up when the device is synchronized with iTunes or iCloud. To get the path to the tmp directory in the application sandbox, you can use the convenience function NSTemporaryDirectory.

     

Constructing a file path

The instances of BNRItem from Homepwner will be saved to a single file in the Documents directory. The BNRItemStore will handle writing to and reading from that file. To do this, the BNRItemStore needs to construct a path to this file.

Implement a new method in BNRItemStore.m to do this.

- (NSString *)itemArchivePath

{

    // Make sure that the first argument is NSDocumentDirectory

    // and not NSDocumentationDirectory

    NSArray *documentDirectories =

        NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,

                                            NSUserDomainMask, YES);

    // Get the one document directory from that list

    NSString *documentDirectory = [documentDirectories firstObject];

    return [documentDirectory stringByAppendingPathComponent:@"items.archive"];

}

The function NSSearchPathForDirectoriesInDomains searches the filesystem for a path that meets the criteria given by the arguments. On iOS, the last two arguments are always the same. (This function is borrowed from OS X, where there are significantly more options.) The first argument is a constant that specifies the directory in the sandbox you want the path to. For example, searching for NSCachesDirectory will return the Caches directory in the application’s sandbox.

You can search the documentation for one of the constants – like NSDocumentDirectory – to locate the other options. Remember that these constants are shared by iOS and OS X, so not all of them will work on iOS.

The return value of NSSearchPathForDirectoriesInDomains is an array of strings. It is an array of strings because, on OS X, there may be multiple paths that meet the search criteria. On iOS, however, there will only be one (if the directory you searched for is an appropriate sandbox directory). Therefore, the name of the archive file is appended to the first and only path in the array. This will be where the archive of BNRItem instances will live.

NSKeyedArchiver and NSKeyedUnarchiver

You now have a place to save data on the filesystem and a model object that can be saved to the filesystem. The final two questions are: how do you kick off the saving and loading processes and when do you do it? To save instances of BNRItem, you will use the class NSKeyedArchiver when the application “exits.”

In BNRItemStore.h, declare a new method.

- (BOOL)saveChanges;

Implement this method in BNRItemStore.m to send the message archiveRootObject:toFile: to the NSKeyedArchiver class.

- (BOOL)saveChanges

{

    NSString *path = [self itemArchivePath];

    // Returns YES on success

    return [NSKeyedArchiver archiveRootObject:self.privateItems

                                       toFile:path];

}

The archiveRootObject:toFile: method takes care of saving every single BNRItem in privateItems to the itemArchivePath. Yes, it is that simple. Here is how archiverRootObject:toFile: works:

·         The method begins by creating an instance of NSKeyedArchiver. (NSKeyedArchiver is a concrete subclass of the abstract class NSCoder.)

·        privateItems is sent the message encodeWithCoder: and is passed the instance of NSKeyedArchiver as an argument.

·        The privateItems array then sends encodeWithCoder: to all of the objects it contains, passing the same NSKeyedArchiver. Thus, all your instances of BNRItem encode their instance variables into the very same NSKeyedArchiver (Figure 18.4).

·        The NSKeyedArchiver writes the data it collected to the path.

Figure 18.4  Archiving the privateItems array

Archiving the privateItems array

When the user presses the Home button on the device, the message applicationDidEnterBackground: is sent to the BNRAppDelegate. That is when you want to send saveChanges to the BNRItemStore.

In BNRAppDelegate.m, implement applicationDidEnterBackground: to kick off saving the items. Make sure to import the header file for BNRItemStore at the top of this file.

#import "BNRItemStore.h"

@implementation BNRAppDelegate

- (void)applicationDidEnterBackground:(UIApplication *)application

{

    BOOL success = [[BNRItemStore sharedStore] saveChanges];

    if (success) {

        NSLog(@"Saved all of the BNRItems");

    }

    else {

        NSLog(@"Could not save any of the BNRItems");

    }

}

(This method may have already been implemented by the template. If so, make sure to add code to the existing method instead of writing a brand new one.)

Build and run the application on the simulator. Create a few instances of BNRItem. Then, press the Home button to leave the application. Check the console, and you should see a log statement indicating that the items were saved.

While you cannot yet load these instances of BNRItem back into the application, you can still verify that something was saved. In Finder, press Command-Shift-G. Then, type in ~/Library/Application Support/iPhone Simulator and press Enter. This is where all of the applications and their sandboxes are stored for the simulator.

Open the directory 7.0 (or, if you are working with another version of iOS, select that directory). Open Applications to see the list of every application that has run on your simulator using iOS 7.0. Unfortunately, these applications have really unhelpful names. You have to dig into each directory to find the one that contains Homepwner.

Figure 18.5  Homepwner’s sandbox

Homepwner’s sandbox

In Homepwner’s directory, navigate into the Documents directory (Figure 18.5). You will see the items.archive file. Here is a tip: make an alias to the iPhone Simulator directory somewhere convenient to make it easy to check the sandboxes of your applications.

Now let’s turn to loading these files. To load instances of BNRItem when the application launches, you will use the class NSKeyedUnarchiver when the BNRItemStore is created.

In BNRItemStore.m, add the following code to initPrivate.

- (instancetype)initPrivate

{

    self = [super init];

    if (self) {

        _privateItems = [[NSMutableArray alloc] init];

        NSString *path = [self itemArchivePath];

        _privateItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

        // If the array hadn't been saved previously, create a new empty one

        if (!_privateItems) {

            _privateItems = [[NSMutableArray alloc] init];

        }

    }

    return self;

}

The unarchiveObjectWithFile: method will create an instance of NSKeyedUnarchiver and load the archive located at the itemArchivePath into that instance. The NSKeyedUnarchiver will then inspect the type of the root object in the archive and create an instance of that type. In this case, the type will be an NSMutableArray because you created this archive with a root object of this type. (If the root object was a BNRItem instead, unarchiveObjectWithFile: would return an instance of BNRItem.)

The newly allocated NSMutableArray is then sent initWithCoder: and, as you may have guessed, the NSKeyedUnarchiver is passed as the argument. The array starts decoding its contents (instances of BNRItem) from the NSKeyedUnarchiver and sends each of these objects the message initWithCoder:, passing the same NSKeyedUnarchiver.

You can now build and run the application. Any items that a user enters will be available until the user explicitly deletes them. One thing to note about testing your saving and loading code: If you kill Homepwner from Xcode, applicationDidEnterBackground: will not get a chance to be called and the item array will not be saved. You must press the Home button first and then kill it from Xcode by clicking the Stop button.

Now that you can save and load items, there is no reason to auto-populate each one with random data. In BNRItemStore.m, modify the implementation of createItem so that it creates an empty BNRItem instead of one with random data.

- (BNRItem *)createItem

{

    BNRItem *item = [BNRItem randomItem];

    BNRItem *item = [[BNRItem alloc] init];

    [self.privateItems addObject:item];

    return item;

}

Application States and Transitions

In Homepwner, the items are archived when the application enters the background state. It is useful to understand all of the states an application can be in, what causes them to transition between states, and how your code can be notified of these transitions. This information is summarized inFigure 18.6.

Figure 18.6  States of typical application

States of typical application

When an application is not running, it is in the not running state, and it does not execute any code or have any memory reserved in RAM.

After the user launches an application, it enters the active state. When in the active state, an application’s interface is on the screen, it is accepting events, and its code is handling those events.

While in the active state, an application can be temporarily interrupted by a system event like an SMS message, push notification, phone call, or alarm. An overlay will appear on top of your application to handle this event. This state is known as the inactive state. In the inactive state, an application is mostly visible (an alert view will appear and obscure part of the interface) and is executing code, but it is not receiving events. Applications typically spend very little time in the inactive state. You can force an active application into the inactive state by pressing the Lock button at the top of the device. The application will stay inactive until the device is unlocked.

When the user presses the Home button or switches to another application in some other way, the application enters the background state. (Actually, it spends a brief moment in the inactive state before transitioning to the background.) In the background, an application’s interface is not visible or receiving events, but it can still execute code. By default, an application that enters the background state has about ten seconds before it enters the suspended state. Your application should not rely on this number; instead it should save user data and release any shared resources as quickly as possible.

An application in the suspended state cannot execute code, you cannot see its interface, and any resources it does not need while suspended are destroyed. A suspended application is essentially freeze-dried and can be quickly thawed when the user relaunches it. Table 18.1 summarizes the characteristics of the different application states.

Table 18.1  Application states

State

Visible

Receives Events

Executes Code

Not Running

No

No

No

Active

Yes

Yes

Yes

Inactive

Mostly

No

Yes

Background

No

No

Yes

Suspended

No

No

No

You can see what applications are in the background or suspended by double-clicking the Home button to get to the multitasking display. (Recently run applications that have been terminated may also appear in this display.)

Figure 18.7  Background and suspended applications in the multitasking display

Background and suspended applications in the multitasking display

An application in the suspended state will remain in that state as long as there is adequate system memory. When the operating system decides memory is getting low, it terminates suspended applications as needed. A suspended application gets no notification that it is about to be terminated; it is simply removed from memory. (An application may remain in the multitasking display after it has been terminated, but it will have to be relaunched when tapped.)

When an application changes its state, the application delegate is sent a message. Here are some of the messages from the UIApplicationDelegate protocol that announce application state transitions. (These are also shown in Figure 18.6.)

- (BOOL)application:(UIApplication *)app

        didFinishLaunchingWithOptions:(NSDictionary *)options

- (void)applicationDidBecomeActive:(UIApplication *)app;

- (void)applicationWillResignActive:(UIApplication *)app;

- (void)applicationDidEnterBackground:(UIApplication *)app;

- (void)applicationWillEnterForeground:(UIApplication *)app;

You can implement code in these methods to take the appropriate actions for your application. Transitioning to the background state is a good place to save any outstanding changes and the state of the application, because it is the last time your application can execute code before it enters the suspended state. Once in the suspended state, an application can be terminated at the whim of the operating system.

Writing to the Filesystem with NSData

Your archiving in Homepwner saves and loads the itemKey for each BNRItem, but what about the images themselves? Let’s extend the image store to save images as they are added and fetch them as they are needed.

The images for BNRItem instances should also be stored in the Documents directory. You can use the image key generated when the user takes a picture to name the image in the filesystem.

Open BNRImageStore.m and add a new method declaration to the class extension.

- (NSString *)imagePathForKey:(NSString *)key;

Implement imagePathForKey: in BNRImageStore.m to create a path in the documents directory using a given key.

- (NSString *)imagePathForKey:(NSString *)key

{

    NSArray *documentDirectories =

            NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,

                                                NSUserDomainMask,

                                                YES);

    NSString *documentDirectory = [documentDirectories firstObject];

    return [documentDirectory stringByAppendingPathComponent:key];

}

To save and load an image, you are going to copy the JPEG representation of the image into a buffer in memory. Instead of just malloc’ing a buffer, Objective-C programmers have a handy class to create, maintain, and destroy these sorts of buffers – NSData. An NSData instance holds some number of bytes of binary data, and you will use NSData to store image data.

In BNRImageStore.m, modify setImage:forKey: to get a path and save the image.

- (void)setImage:(UIImage *)image forKey:(NSString *)key

{

    self.dictionary[key] = image;

    // Create full path for image

    NSString *imagePath = [self imagePathForKey:key];

    // Turn image into JPEG data

    NSData *data = UIImageJPEGRepresentation(image, 0.5);

    // Write it to full path

    [data writeToFile:imagePath atomically:YES];

}

Let’s examine this code more closely. The function UIImageJPEGRepresentation takes two parameters: a UIImage and a compression quality. The compression quality is a float from 0 to 1, where 1 is the highest quality (least compression). The function returns an instance of NSData.

This NSData instance can be written to the filesystem by sending it the message writeToFile:atomically:. The bytes held in this NSData are then written to the path specified by the first parameter. The second parameter, atomically, is a Boolean value. If it is YES, the file is written to a temporary place on the filesystem, and, once the writing operation is complete, that file is renamed to the path of the first parameter, replacing any previously existing file. Writing atomically prevents data corruption should your application crash during the write procedure.

It is worth noting that this way of writing data to the filesystem is not archiving. While NSData instances can be archived, using the method writeToFile:atomically: copies the bytes in the NSData directly to the filesystem.

In BNRImageStore.m, make sure that when an image is deleted from the store, it is also deleted from the filesystem:

- (void)deleteImageForKey:(NSString *)key

{

    if (!key) {

        return;

    }

    [self.dictionary removeObjectForKey:key];

    NSString *imagePath = [self imagePathForKey:key];

    [[NSFileManager defaultManager] removeItemAtPath:imagePath

                                               error:nil];

}

Now that the image is stored in the filesystem, the BNRImageStore will need to load that image when it is requested. The class method imageWithContentsOfFile: of UIImage will read in an image from a file, given a path.

In BNRImageStore.m, replace the method imageForKey: so that the BNRImageStore will load the image from the filesystem if it does not already have it.

- (UIImage *)imageForKey:(NSString *)key

{

    return self.dictionary[key];

    // If possible, get it from the dictionary

    UIImage *result = self.dictionary[key];

    if (!result) {

        NSString *imagePath = [self imagePathForKey:key];

        // Create UIImage object from file

        result = [UIImage imageWithContentsOfFile:imagePath];

        // If we found an image on the file system, place it into the cache

        if (result) {

            self.dictionary[key] = result;

        }

        else {

            NSLog(@"Error: unable to find %@", [self imagePathForKey:key]);

        }

    }

    return result;

}

Build and run the application again. Take a photo for an item, exit the application, and then press the Home button. Launch the application again. Selecting that same item will show all its saved details – including the photo you just took.

Also, notice that the images were saved immediately after being taken, while the instances of BNRItem were saved only when the application entered the background. You save the images right away because they are just too big to keep in memory for long.

NSNotificationCenter and Low-Memory Warnings

When the system is running low on RAM, it issues a low memory warning to the running application. The application responds by freeing up any resources that it does not need at the moment and can easily recreate. View controllers, during a low memory warning, are sent the messagedidReceiveMemoryWarning.

Objects other than view controllers may have data that they are not using and can recreate later. The BNRImageStore is such an object – when a low-memory warning occurs, it can release its ownership of the images by emptying its dictionary. Then if another object ever asks for a specific image again, that image can be loaded into memory from the filesystem.

In order to have objects that are not view controllers respond to low memory warnings, you must use the notification center. Every application has an instance of NSNotificationCenter, which works like a smart bulletin board. An object can register as an observer (“Send me ‘lost dog’notifications”). When another object posts a notification (“I lost my dog”), the notification center forwards the notification to the registered observers.

Whenever a low-memory warning occurs, UIApplicationDidReceiveMemoryWarningNotification is posted to the notification center. Objects that want to implement their own low-memory warning handlers can register for this notification.

In BNRImageStore.m, edit the initPrivate method to register the image store as an observer of this notification.

- (instancetype)initPrivate

{

    self = [super init];

    if (self) {

        _dictionary = [[NSMutableDictionary alloc] init];

        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

        [nc addObserver:self

               selector:@selector(clearCache:)

                   name:UIApplicationDidReceiveMemoryWarningNotification

                 object:nil];

    }

    return self;

}

Now your image store is registered as an observer with the notification center (Figure 18.8).

Figure 18.8  Registered as an observer with notification center

Registered as an observer with notification center

Now, when a low-memory warning is posted, the notification center will send the message clearCache: to the BNRImageStore instance (Figure 18.9).

Figure 18.9  Receiving the notification

Receiving the notification

In BNRImageStore.m, implement clearCache: to remove all the instances of UIImage from the BNRImageStore’s dictionary.

- (void)clearCache:(NSNotification *)note

{

    NSLog(@"flushing %d images out of the cache", [self.dictionary count]);

    [self.dictionary removeAllObjects];

}

Removing an object from a dictionary relinquishes ownership of the object, so flushing the cache causes all of the images to lose an owner. Images that are not being used by other objects are destroyed, and when they are needed again, they will be reloaded from the filesystem. If an image is currently displayed in the BNRDetailViewController’s imageView, then it will not be destroyed since it is owned by the imageView. When the BNRDetailViewController’s imageView loses ownership of that image (either because the BNRDetailViewController was popped off the stack or a new image was chosen), then it is destroyed. It will be reloaded later if needed.

Build and run the app in the simulator. Create or load in some images. Then select Simulate Memory Warning in the Hardware menu. You should see a log statement indicating that the cache has been flushed out.

More on NSNotificationCenter

Notifications are another form of callbacks, like delegation and target-action pairs. However, unlike delegation and target-action pairs, which require that the object responsible for an event send a message directly to its delegate or targets, notifications use a middle-man: theNSNotificationCenter.

Notifications in Objective-C are represented by instances of NSNotification. Each NSNotification has a name (used by the notification center to find observers), an object (the object that is responsible for posting the notification), and an optional user info dictionary that contains additional information that the poster wants observers to know about. For example, if for some reason the status bar’s frame changes, UIApplication posts a UIApplicationDidChangeStatusBarFrameNotification with a user info dictionary. In the dictionary is the new frame of the status bar. If you received the notification, you could get the frame like this:

- (void)statusBarMovedOrResized:(NSNotification *)note

{

    NSDictionary *userInfo = [note userInfo];

    NSValue *wrappedRect = userInfo[UIApplicationStatusBarFrameUserInfoKey];

    CGRect newFrame = [wrappedRect CGRectValue];

    ...use frame here...

}

Notice that the CGRect had to be wrapped in an NSValue because only objects can go into dictionaries.

How can you know what is in the userInfo dictionary? Each notification is documented in the class reference. Most say “This notification does not contain a userInfo dictionary.” For notifications with userInfo dictionaries, all the keys and what they map to will be listed.

The last argument of addObserver:selector:name:object: is typically nil – which means, no matter what object posted a “Fire!” notification, the observer will get sent its message. You can specify a pointer to an object for this argument and the observer will only get notified if that object posts the notification it has registered for, while ignoring any other object that posts the same notification.

One purpose of the notification center is to allow multiple objects to register a callback for the same event. Any number of objects can register as an observer for the same notification name. When the notification occurs, all of those objects are sent the message they registered with (in no particular order). Thus, notifications are a good solution when more than one object is interested in an event. For example, many objects might want to know about a rotation event, so Apple used notifications for that.

One final point: the NSNotificationCenter has nothing to do with inter-app communication, push notifications, or local notifications. It is simply for communication between objects in a single application.

Model-View-Controller-Store Design Pattern

In this exercise, you expanded on the BNRItemStore to allow it to save and load BNRItem instances from the filesystem. The controller object asks the BNRItemStore for the model objects it needs, but it does not have to worry about where those objects actually came from. As far as the controller is concerned, if it wants an object, it will get one; the BNRItemStore is responsible for making sure that happens.

The standard Model-View-Controller design pattern calls for the controller to be bear the burden of saving and loading model objects. However, in practice, this can become overwhelming – the controller is simply too busy handling the interactions between model and view objects to deal with the details of how objects are fetched and saved. Therefore, it is useful to move the logic that deals with where model objects come from and where they are saved to into another type of object: a store.

A store exposes a number of methods that allow a controller object to fetch and save model objects. The details of where these model objects come from or how they get there is left to the store. In this chapter, the store worked with a simple file. However, the store could also access a database, talk to a web service, or use some other method to produce the model objects for the controller.

One benefit of this approach, besides simplified controller classes, is that you can swap out how the store works without modifying the controller or the rest of your application. This can be a simple change, like the directory structure of the data, or a much larger change, like the format of the data. Thus, if an application has more than one controller object that needs to save and load data, you only have to change the store object.

Many developers talk about the Model-View-Controller design pattern. In this chapter, we have extended the idea to a Model-View-Controller-Store design pattern.

Bronze Challenge: PNG

Instead of saving each image as a JPEG, save it as a PNG.

For the More Curious: Application State Transitions

Let’s write some quick code to get a better understanding of the different application state transitions.

You already know about self, an implicit variable that points to the instance that is executing the current method. There is another implicit variable called _cmd, which is the selector for the current method. You can get the NSString representation of a selector with the functionNSStringFromSelector.

In BNRAppDelegate.m, implement the application state transition delegate methods so that they print out the name of the method. You will need to add four more methods. (Check to make sure the template has not already created these methods before writing brand new ones.)

- (void)applicationWillResignActive:(UIApplication *)application

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

}

- (void)applicationWillEnterForeground:(UIApplication *)application

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

}

- (void)applicationDidBecomeActive:(UIApplication *)application

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

}

- (void)applicationWillTerminate:(UIApplication *)application

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

}

Now, add the following NSLog statements to the top of application:didFinishLaunchingWithOptions: and applicationDidEnterBackground:.

- (BOOL)application:(UIApplication *)application

   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

    ...

}

- (void)applicationDidEnterBackground:(UIApplication *)application

{

    NSLog(@"%@", NSStringFromSelector(_cmd));

    [[BNRItemStore sharedStore] saveChanges];

}

Build and run the application. You will see that the application gets sent application:didFinishLaunchingWithOptions: and then applicationDidBecomeActive:. Play around some to see what actions cause what transitions.

Press the Home button, and the console will report that the application briefly inactivated and then went to the background state. Relaunch the application by tapping its icon on the Home screen or in the multitasking display. The console will report that the application entered the foreground and then became active.

Press the Home button to exit the application again. Then, double-click the Home button to open the multitasking display. Swipe the Homepwner application up and off this display to quit the application. Note that no message is sent to your application delegate at this point – it is simply terminated.

For the More Curious: Reading and Writing to the Filesystem

In addition to archiving and NSData’s binary read and write methods, there are a few more methods for transferring data to and from the filesystem. One of them, Core Data, is coming up in Chapter 23. A couple of others are worth mentioning here.

You have access to the standard file I/O functions from the C library. These functions look like this:

FILE *inFile = fopen("textfile", "rt");

char *buffer = malloc(someSize);

fread(buffer, byteCount, 1, inFile);

FILE *outFile = fopen("binaryfile", "w");

fwrite(buffer, byteCount, 1, outFile);

However, you will not see these functions used much because there are more convenient ways of reading and writing binary and text data. Using NSData works well for binary data. For text data, NSString has two instance methods writeToFile:atomically:encoding:error: andinitWithContentsOfFile:. They are used as follows:

// A local variable to store an error object if one comes back

NSError *err;

NSString *someString = @"Text Data";

BOOL success = [someString writeToFile:@"/some/path/file"

                            atomically:YES

                              encoding:NSUTF8StringEncoding

                                 error:&err];

if (!success) {

    NSLog(@"Error writing file: %@", [err localizedDescription]);

}

NSString *myEssay = [[NSString alloc] initWithContentsOfFile:@"/some/path/file"

                                                    encoding:NSUTF8StringEncoding

                                                       error:&err];

if (!myEssay) {

    NSLog(@"Error reading file: %@", [err localizedDescription]);

}

What is that NSError object? Some methods might fail for a variety of reasons. For example, writing to the filesystem might fail because the path is invalid or the user does not have permission to write to the specified path. An NSError object contains the reason for a failure. You can send the message localizedDescription to an instance of NSError for a human-readable description of the error. This is something you can show to the user or print to a debug console.

The syntax for getting back an NSError instance is a little strange. An error object is only created if an error occurred; otherwise, there is no need for the object. When a method can return an error through one of its arguments, you create a local variable that is a pointer to an NSError object. Notice that you do not instantiate the error object – that is the job of the method you are calling. Instead, you pass the address of your pointer variable (&err) to the method that might generate an error. If an error occurs in the implementation of that method, an NSError instance is created, and your pointer is set to point at that new object. If you do not care about the error object, you can always pass nil.

Sometimes you want to show the error to the user. This is typically done with a UIAlertView (Figure 18.10).

Figure 18.10  Example of UIAlertView

Example of UIAlertView

Creating a UIAlertView looks like this:

NSString *x = [[NSString alloc] initWithContentsOfFile:@"/some/path/file"

                                              encoding:NSUTF8StringEncoding

                                                 error:&err];

if (!x) {

    UIAlertView *a = [[UIAlertView alloc] initWithTitle:@"Read Failed"

                                                message:[err localizedDescription]

                                               delegate:nil

                                      cancelButtonTitle:@"OK"

                                      otherButtonTitles:nil];

    [a show];

}

Note that in many languages, anything unexpected results in an exception being thrown. Among Objective-C programmers, exceptions are nearly always used to indicate programmer error. When an exception is thrown, the information about what went wrong is in an NSException object. That information is usually just a hint to the programmer like “You tried to access the 7th object in this array, but there are only two.” The symbols for the call stack (as it appeared when the exception was thrown) are also in the NSException.

When do you use NSException and when do you use NSError? If you are writing a method that should only be called with an odd number as an argument, throw an exception if it is called with an even number – the caller is making an error and you want to help that programmer find the error in his ways. If you are writing a method that wants to read the contents of a particular directory, but does not have the necessary privileges, create an NSError and pass it back to the caller to indicate why you were unable to fulfill this very reasonable request.

Like NSString, the classes NSDictionary and NSArray have writeToFile: and initWithContentsOfFile: methods. To write collection objects to the filesystem with these methods, the collection objects must contain only property list serializable objects. The only objects that are property list serializable are NSStringNSNumberNSDateNSDataNSArray, and NSDictionary. When an NSArray or NSDictionary is written to the filesystem with these methods, an XML property list is created. An XML property list is a collection of tagged values:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"

       "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<array>

    <dict>

        <key>firstName</key>

        <string>Christian</string>

        <key>lastName</key>

        <string>Keur</string>

    </dict>

    <dict>

        <key>firstName</key>

        <string>Joe</string>

        <key>lastName</key>

        <string>Conway</string>

    </dict>

    <dict>

        <key>firstName</key>

        <string>Aaron</string>

        <key>lastName</key>

        <string>Hillegass</string>

    </dict>

</array>

</plist>

XML property lists are a convenient way to store data because they can be read on nearly any system. Many web service applications use property lists as input and output. The code for writing and reading a property list looks like this:

NSMutableDictionary *d = [NSMutableDictionary dictionary];

d[@"String"] = @"A string";

[d writeToFile:@"/some/path/file" atomically:YES];

NSMutableDictionary *anotherD = [[NSMutableDictionary alloc]

                            initWithContentsOfFile:@"/some/path/file"];

For the More Curious: The Application Bundle

When you build an iOS application project in Xcode, you create an application bundle. The application bundle contains the application executable and any resources you have bundled with your application. Resources are things like XIB files, images, audio files – any files that will be used at runtime. When you add a resource file to a project, Xcode is smart enough to realize that it should be bundled with your application.

How can you tell which files are being bundled with your application? Select the Homepwner project from the project navigator. Check out the Build Phases pane in the Homepwner target. Everything under Copy Bundle Resources will be added to the application bundle when it is built.

Each item in the Homepwner target group is one of the phases that occurs when you build a project. The Copy Bundle Resources phase is where all of the resources in your project get copied into the application bundle.

You can check out what an application bundle looks like on the filesystem after you install an application on the simulator. Navigate to ~/Library/Application Support/iPhone Simulator/(version number)/Applications. The directories within this directory are the application sandboxes for applications installed on your computer’s iOS simulator. Opening one of these directories will show you what you expect in an application sandbox: an application bundle and the Documents, tmp, and Library directories. Right- or Command-click the application bundle and choose Show Package Contents from the contextual menu (Figure 18.11).

Figure 18.11  Viewing an application bundle

Viewing an application bundle

A Finder window will appear showing you the contents of the application bundle (Figure 18.12). When a user downloads your application from the App Store, these files are copied to their device.

Figure 18.12  The application bundle

The application bundle

You can load files from the application’s bundle at runtime. To get the full path for files in the application bundle, you need to get a pointer to the application bundle and then ask it for the path of a resource.

// Get a pointer to the application bundle

NSBundle *applicationBundle = [NSBundle mainBundle];

// Ask for the path to a resource named myImage.png in the bundle

NSString *path = [applicationBundle pathForResource:@"myImage"

                                             ofType:@"png"];

If you ask for the path to a file that is not in the application’s bundle, this method will return nil. If the file does exist, then the full path is returned, and you can use this path to load the file with the appropriate class.

Also, files within the application bundle are read-only. You cannot modify them nor can you dynamically add files to the application bundle at runtime. Files in the application bundle are typically things like button images, interface sound effects, or the initial state of a database you ship with your application. You will use this method in later chapters to load these types of resources at runtime.