iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (54 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
2.99Mb size Format: txt, pdf, ePub
Writing to the Filesystem with NSData

Our archiving in
Homepwner
saves and loads the
imageKey
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 are created by user interaction and are only stored within the application. Therefore, the
Documents
directory is the best place to store them. You can use the image key generated when the user takes a picture to name the image in the filesystem.

 

Open
BNRImageStore.h
and add a new method declaration.

 
- (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 objectAtIndex:0];
    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
store image data.

 

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

 
- (void)setImage:(UIImage *)i forKey:(NSString *)s
{
    [dictionary setObject:i forKey:s];
    
// Create full path for image
    NSString *imagePath = [self imagePathForKey:s];
    // Turn image into JPEG data,
    NSData *d = UIImageJPEGRepresentation(i, 0.5);
    // Write it to full path
    [d 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. 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:
is a binary write 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 *)s
{
    if (!s)
        return;
    [dictionary removeObjectForKey:s];
    
NSString *path = [self imagePathForKey:s];
    [[NSFileManager defaultManager] removeItemAtPath:path
                                               error:NULL];
}
 

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 doesn’t already have it.

 
- (UIImage *)imageForKey:(NSString *)s
{
    
return [dictionary objectForKey:s];
    
    // If possible, get it from the dictionary
    UIImage *result = [dictionary objectForKey:s];
    if (!result) {
        // Create UIImage object from file
        result = [UIImage imageWithContentsOfFile:[self imagePathForKey:s]];
        // If we found an image on the file system, place it into the cache
        if (result)
            [dictionary setObject:result forKey:s];
        else
            NSLog(@"Error: unable to find %@", [self imagePathForKey:s]);
    }
    return result;
}
 

When a
BNRItem
is removed from the store, its image should also be removed from the filesystem. At the top of
BNRItemStore.m
, import the header for the
BNRImageStore
and add the following code to
removeItem:
.

 
#import "BNRImageStore.h"
@implementation BNRItemStore
- (void)removeItem:(BNRItem *)p
{
    NSString *key = [p imageKey];
    [[BNRImageStore sharedStore] deleteImageForKey:key];
    [allItems removeObjectIdenticalTo:p];
}
 

Build and run the application again. Take a photo for a item, exit the application, and then kill it from the dock. 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
BNRItem
s were saved only when the application entered background. We saved the images right away because they are just too big to keep in memory for long.

 
More on Low-Memory Warnings

You have seen how view controllers handle low-memory warnings – they are sent the message
didReceiveMemoryWarning
and destroy their views if they are not on the screen. This is an appropriate solution to handling a low-memory warning: an object gets rid of anything it isn’t currently using and can recreate later. Objects other than view controllers may have data that they aren’t using and can recreate later. The
BNRImageStore
is such an object – when its images aren’t on the screen, it is okay to destroy them because they can be loaded from the filesystem when they’re needed again.

 

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
init
method to register the image store as an observer of this notification.

 
- (id)init
{
    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, a low-memory warning will send the message
clearCache:
to the
BNRImageStore
instance. In
BNRImageStore.m
, implement
clearCache:
to remove all the
UIImage
objects from the
BNRImageStore
’s
dictionary
.

 
- (void)clearCache:(NSNotification *)note
{
    NSLog(@"flushing %d images out of the cache", [dictionary count]);
    [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 aren’t 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
DetailViewController
’s
imageView
, then it will not be destroyed since it is owned by the
imageView
. When the
DetailViewController
’s
imageView
loses ownership of that image (either because the
DetailViewController
was popped off the stack or a new image was taken), then it is destroyed. It will be reloaded later if needed.

 
Model-View-Controller-Store Design Pattern

In this exercise, we 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 doesn’t 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.

 

You can also apply the idea of a store to objects like
CLLocationManager
. The location manager is a store that returns model objects of type
CLLocation
. The basic idea still stands: a model object is returned to the controller, and the controller doesn’t care where it came from.

 

Thus, we introduce a new design pattern called
Model-View-Controller-Store
, or simply MVCS. It’s the hip, new design pattern that programmers are talking about everywhere. This design pattern will be expanded on in
Chapter 28
.

 
Bronze Challenge: PNG

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

 

Other books

Historias desaforadas by Adolfo Bioy Casares
The End of Doom by Ronald Bailey
The Marshal's Ready-Made Family by Sherri Shackelford
Fade To Midnight by Shannon McKenna
Avenge by Sarah M. Ross