Chapter 10

Data Storage Recipes

When working in iOS, one of the most important topics to understand is the concept, use, and implementation of persistence. This term refers to the idea of having information be saved and retrieved, or “persist,” through the closing or restarting of an application. Just as pages from books written thousands of years ago can still be read, we are able to make use of certain key concepts in iOS to allow our information, from the simplest of values to the most complex of data structures, to stay stored in our device for indefinite periods of time. We will cover a variety of methods of persistence throughout this chapter with different advantages, disadvantages, general uses, and complexities, so that we can develop a full understanding of the best method of storage for any given situation.

Recipe 10–1: Using NSUserDefaults

When developing applications, we very often run into issues where we simply need to store simple values, such as strings, numbers, or Boolean values. While there are a variety of ways to store data, the easiest of these is NSUserDefaults, built specifically for such combinations.

The NSUserDefaults class is a simple implementation used to store basic values, such as instances of NSString, NSNumber, BOOL, etc. It can also be used to store more complex data structures, such as NSArray or NSDictionary, as long as they do not contain massive amounts of data. Any kind of image should not be stored with NSUserDefaults. In this way, it is excellent for storing any kind of preference or option for an application.

Start off by creating a new project called “Stubborn” (since you want your information to stick around).

Select the Single View Application template to create a simple application for you to configure. After entering your name and ensuring the device is set to the iPhone family, as in Figure 10–1, click through to finish creating your project.

Image

Figure 10–1. Configuring your Stubborn project

Now, in your newly created view controller’s XIB file, you will start off by setting up your basic user interface. Drag and drop three UILabels, two UITextFields, a UISwitch, and a UIActivityIndicatorView so as to create the view shown in Figure 10–2.

Image

Figure 10–2. Your view controller’s XIB for storing values

As you can probably guess, you will simply be using text fields to set the text of your labels, and using the switch to control whether the activity indicator view is animating.

Next, connect each of these elements in your XIB file to a property in your view controller’s header file by holding ^ (Ctrl), and click-dragging from each element into your header file. This will automatically create your property, synthesize it, and add a statement to nullify it in the application’s -viewDidUnload method. The following header file excerpt shows the property names you will use to manage each element.

#import <UIKit/UIKit.h>

@interface MainViewController : UIViewController

@property (strong, nonatomic) IBOutlet UILabel *firstLabel;
@property (strong, nonatomic) IBOutlet UILabel *secondLabel;
@property (strong, nonatomic) IBOutlet UILabel *animateLabel;
@property (strong, nonatomic) IBOutlet UITextField *firstNameTextField;
@property (strong, nonatomic) IBOutlet UITextField *lastNameTextField;
@property (strong, nonatomic) IBOutlet UISwitch *animateSwitch;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

Up next, you need to conform your view controller to the UITextFieldDelegate protocol in order to gain more control over the actions of each UITextField. The header line of your view controller’s header file (the second line in the preceding code) will now look like so:

@interface MainViewController : UIViewController<UITextFieldDelegate>

Next, update your -viewDidLoad method in your view controller’s interface file to set both text field delegates.

- (void)viewDidLoad
{
    [super viewDidLoad];
self.firstNameTextField.delegate = self;
self.lastNameTextField.delegate = self;
}

Now you can write a method to access the NSUserDefaults class and save the desired values of your view.

(void)updateDefaults
{
//Acquire Values
NSString *first = self.firstNameTextField.text;
NSString *last = self.lastNameTextField.text;
BOOL animating = self.activityIndicator.isAnimating;

//Acquire Shared Instance
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//Set Objects/Values to Persist
    [userDefaults setObject:first forKey:@"firstName"];
    [userDefaults setObject:last forKey:@"lastName"];
    [userDefaults setBool:animating forKey:@"animating"];

//Save Changes
    [userDefaults synchronize];
}

As shown in this method, it is always important to remember to call the -synchronize method when you have finished making changes to the NSUserDefaults object in order to save your data.

Along with the +standardUserDefaults method, which retrieves a shared instance of the NSUserDefaults class, this class also has a class method, +resetStandardUserDefaults, used to completely wipe all saved stored values for an application.

You can implement your UITextFieldDelegate protocol methods to now handle the entering of data to automatically save newly entered text.

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
return NO;
}

-(void)textFieldDidEndEditing:(UITextField *)textField
{
if (textField == self.firstNameTextField)
    {
self.firstLabel.text = textField.text;
    }
else if (textField == self.lastNameTextField)
    {
self.secondLabel.text = textField.text;
    }
    [self updateDefaults];
}

For your UISwitch, you will also create a method to handle the changing of its value.

-(void)switchValueChanged:(UISwitch *)sender
{
if (sender.on)
    {
        [self.activityIndicator startAnimating];
self.animateLabel.text = @"Animating";
    }
else
    {
        [self.activityIndicator stopAnimating];
self.animateLabel.text = @"Stopped";
    }
    [self updateDefaults];
}

In order to assign this method to be called by your UISwitch, you need to modify your -viewDidLoad again.

- (void)viewDidLoad
{
    [super viewDidLoad];
self.firstNameTextField.delegate = self;
self.lastNameTextField.delegate = self;

    [self.animateSwitch addTarget:self action:@selector(switchValueChanged:)
forControlEvents:UIControlEventValueChanged];
}

At this point, your application should be able to easily save your values entered, but you still need to include functionality to reload these values in case your application is closed. You will create a single method to access the NSUserDefaults class again, check for any stored values, and display them as appropriate.

-(void)setValuesFromDefaults
{
//Acquire Shared Instance
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//Acquire Values
NSString *first = [userDefaults objectForKey:@"firstName"];
NSString *last = [userDefaults objectForKey:@"lastName"];
BOOL animating = [userDefaults boolForKey:@"animating"];

//Display Values Appropriately
if (first != nil)
    {
self.firstNameTextField.text = first;
self.firstLabel.text = first;
    }
if (last != nil)
    {
self.lastNameTextField.text = last;
self.secondLabel.text = last;
    }
if (animating)
    {
self.animateLabel.text = @"Animating";
if (self.activityIndicator.isAnimating == NO)
        {
            [self.activityIndicator startAnimating];
        }
    }
else
    {
self.animateLabel.text = @"Stopped";
if (self.activityIndicator.isAnimating == YES)
        {
            [self.activityIndicator stopAnimating];
        }
    }
    [self.animateSwitch setOn:animating animated:NO];
}

Finally, you just need to adjust your -viewDidLoad method again in order to load any saved preferences upon the running of the application.

- (void)viewDidLoad
{
    [super viewDidLoad];
self.firstNameTextField.delegate = self;
self.lastNameTextField.delegate = self;

    [self.animateSwitch addTarget:self action:@selector(switchValueChanged:)
forControlEvents:UIControlEventValueChanged];

    [self setValuesFromDefaults];
}

At this point, your application can successfully save your values! If you run your application on the iOS simulator or on your device, change the values, and then close and reopen it, your values should have been set as they were. Remember that to fully close an application on newer devices you must double-tap the home button, press and hold on the app icons that appear, and then press the “-” mark on the desired app. Keep in mind also to be careful closing an application in this way if running the project through Xcode, as your application may crash. In this case, you should use the Stop button in Xcode to close your app instead. Though you cannot quite tell, Figure 10–3 shows an application that has been closed and reopened multiple times with the values persisting!

Image

Figure 10–3. Your application persisting information

NOTE: Though you did not use a great variety of values to store with NSUserDefaults in this short recipe, there are in fact methods to store almost any type of lightweight value, including Bool, Float, Integer, Double, and URL. For any kind of more complex object, such as an NSString, NSArray, or NSDictionary, you use the general -setObject:forKey: method.

Recipe 10–2: Managing Files

While the NSUserDefaults class is especially useful for doing quick persistence of light data, it is not nearly as efficient for dealing with large objects, such as videos, music, or images. For these more complex items, you can make use of iOS’s file management system.

You will create a new application to display a table of “Hotspots,” which you will be able to edit and add to, while persisting all of your data. While these objects will be fairly lightweight, and thus able to be stored in NSUserDefaults, you will make use of the file management system for demonstration purposes.

To start off, you will build your application's user interface without worrying about data persistence. Create a new project using the Single View Application template, following the exact same process as the previous recipe.

First, you will go through and create your Hotspot class. Create a new file, making sure to select the “Objective-C class” template using the dialog shown in Figure 10–4.

Image

Figure 10–4. Creating an NSObject subclass

On the next screen, enter your file's name as “Hotspot”, and make sure the subclass field is set to “NSObject”. Click through to create your file.

In your Hotspot class's header file, Hotspot.h, define a series of NSString properties to represent the data for this object.

#import <Foundation/Foundation.h>

@interface Hotspot : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *address;
@property (nonatomic, strong) NSString *city;
@property (nonatomic, strong) NSString *state;

@end

In the implementation file Hotspot.m, add a single synthesize command for these four properties.

@synthesize name, city, state, address;

Next, you will create another view controller to manage the creation and editing of any Hotspot objects by the user. Create a new file, and select the “UIViewController subclass” template. Provide the class name “HotspotInfoViewController”, and make sure the subclass field is set to “UIViewController”, as in Figure 10–5. Make sure also that the box marked “With XIB for user interface” is also checked.

Image

Figure 10–5. Configuring your new view controller

In the newly created controller.'s XIB file, HotspotInfoViewController.xib, create your user interface to mirror that shown in Figure 10–6.

Image

Figure 10–6. Configuring the HotspotInfoViewController.xib file

Connect each UITextField element to your view controller's header file using the respective property identifiers textFieldName, textFieldAddress, textFieldCity, and textFieldState. You will not need to define a property for your UIButton, but connect it to an IBAction -saveButtonPressed:. These changes should add the following property and method declarations to your header file.

@property (strong, nonatomic) IBOutlet UITextField *textFieldName;
@property (strong, nonatomic) IBOutlet UITextField *textFieldAddress;
@property (strong, nonatomic) IBOutlet UITextField *textFieldCity;
@property (strong, nonatomic) IBOutlet UITextField *textFieldState;
- (IBAction)saveButtonPressed:(id)sender;

This class will need a Hotspot property to reference the Hotspot currently being viewed. Add an import statement for the Hotspot class.

#import "Hotspot.h"

Declare the Hotspot property.

@property (strong, nonatomic) IBOutlet Hotspot *hotspot;

You will also create a delegate property to handle the completion of this class's use. Define the protocol “HotspotInfoDelegate” by adding the following code above the main “interface” section of the header file.

@class HotspotInfoViewController;
@protocol HotspotInfoDelegate <NSObject>
-(void)HotspotInfoViewController:(HotspotInfoViewController *)hotspotInfoVC
didReturnHotspot:(Hotspot *)hotspot isNew:(BOOL)isNew;
@end

You can now declare your delegate property like so:

@property (strong, nonatomic) id<HotspotInfoDelegate> delegate;

Make sure that both the delegate and hotspot properties are properly synthesized, and that both are set to nil in your controller's -viewDidUnload method.

Finally, you will make this view controller the delegate to all four of the UITextField elements that you added, so make sure this class conforms to the UITextFieldDelegate protocol by adding the <UITextFieldDelegate> code to your interface line.

In its entirety, your HotspotInfoViewController.h file should now look like so:

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

@class HotspotInfoViewController;

@protocol HotspotInfoDelegate <NSObject>
-(void)HotspotInfoViewController:(HotspotInfoViewController *)hotspotInfoVC
didReturnHotspot:(Hotspot *)hotspot isNew:(BOOL)isNew;
@end

@interface HotspotInfoViewController : UIViewController<UITextFieldDelegate>

@property (strong, nonatomic) IBOutlet UITextField *textFieldName;
@property (strong, nonatomic) IBOutlet UITextField *textFieldAddress;
@property (strong, nonatomic) IBOutlet UITextField *textFieldCity;
@property (strong, nonatomic) IBOutlet UITextField *textFieldState;
- (IBAction)saveButtonPressed:(id)sender;

@property (strong, nonatomic) Hotspot *hotspot;

@property (strong, nonatomic) id<HotspotInfoDelegate> delegate;

@end

In your HotspotInfoViewController's implementation file, you will add a method to populate the view with a given Hotspot's information.

-(void)populateWithHotspot
{
self.textFieldName.text = hotspot.name;
self.textFieldAddress.text = hotspot.address;
self.textFieldCity.text = hotspot.city;
self.textFieldState.text = hotspot.state;
}

This method will then be called in your -viewDidLoad method, which will also configure your user interface.

- (void)viewDidLoad
{
    [super viewDidLoad];
self.textFieldName.placeholder = @"Name";
self.textFieldAddress.placeholder = @"Address";
self.textFieldCity.placeholder = @"City";
self.textFieldState.placeholder = @"State";

self.textFieldName.delegate = self;
self.textFieldAddress.delegate = self;
self.textFieldCity.delegate = self;
self.textFieldState.delegate = self;

if (self.hotspot != nil)
    {
        [self populateWithHotspot];
    }
}

If you did not put the code for the -populateWithHotspot method above that of your -viewDidLoad, you will need to add a method signature for the former to your header file.

Include a simple method to dismiss the keyboard when the user is done editing a UITextField

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return NO;
}

Finally, your -saveButtonPressed: will be written to either save the current Hotspot, if one was given, or create a new one. This object will then be passed back to your delegate property through the -HotspotInfoViewController:didReturnHotspot:isNew: method to be properly handled.

- (IBAction)saveButtonPressed:(id)sender
{
BOOL isNew;
if (self.hotspot != nil)
    {
self.hotspot.name = self.textFieldName.text;
self.hotspot.address = self.textFieldAddress.text;
self.hotspot.city = self.textFieldCity.text;
self.hotspot.state = self.textFieldState.text;1
        isNew = NO;
    }
else
    {
Hotspot *newHotspot = [[Hotspot alloc] init];
        newHotspot.name = self.textFieldName.text;
        newHotspot.address = self.textFieldAddress.text;
        newHotspot.city = self.textFieldCity.text;
        newHotspot.state = self.textFieldState.text;
self.hotspot = newHotspot;
        isNew = YES;
    }
    [self.delegate HotspotInfoViewController:self didReturnHotspot:self.hotspot
isNew:isNew];
}

__________

Now, you can build your main view controller. In this class's XIB file, add a UITableView to fill the entire view, resembling Figure 10–7.

Image

Figure 10–7. Adding a UITableView to your main view controller

Connect this UITableView to your header file using the property name tableViewHotspots.

Create an NSMutableArray property in your main view controller's header file to hold the Hotspot objects.

@property (strong, nonatomic) NSMutableArray *hotspots;

Add the following two import statements to this header file.

#import "HotspotInfoViewController.h"
#import "Hotspot.h"

Finally, make sure that this controller is told to conform to the UITableViewDelegate and UITableViewDataSource protocols, as well as the HotspotsInfoDelegate protocol that you created.

After all these changes, your header file should resemble the following:

#import <UIKit/UIKit.h>
#import "HotspotInfoViewController.h"
#import "Hotspot.h"

@interface MainViewController : UIViewController<UITableViewDelegate,
UITableViewDataSource, HotspotInfoDelegate>

@property (strong, nonatomic) IBOutlet UITableView *tableViewHotspots;
@property (strong, nonatomic) NSMutableArray *hotspots;

@end

In the implementation file for this class, after synthesizing the hotspots property, you need to declare a custom getter method to ensure that your array is properly created.

-(NSMutableArray *)hotspots
{
if (!hotspots)
    {
    hotspots = [[NSMutableArray alloc] initWithCapacity:10];
    }
return hotspots;
}

You will write your -viewDidLoad method to set the UITableView's delegate and dataSource properties, as well as configure the user interface to work inside a UINavigationController.

- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"Hotspots";

self.tableViewHotspots.delegate = self;
self.tableViewHotspots.dataSource = self;

UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self
action:@selector(newHotspot:)];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.navigationItem.leftBarButtonItem = addButton;
}

The -newHotspot: selector used will be easily implemented to present an instance of your HotspotInfoViewController, with no hotspot pre-set.

-(void)newHotspot:(id)sender
{
    HotspotInfoViewController *hotspotVC = [[HotspotInfoViewController alloc] init];
    hotspotVC.delegate = self;
    [self.navigationController pushViewController:hotspotVC animated:YES];
}

In order to respond to the completed use of a HotspotInfoViewController, you should implement the HotspotInfoDelegate method that you specified earlier.

-(void)HotspotInfoViewController:(HotspotInfoViewController *)hotspotInfoVC
didReturnHotspot:(Hotspot *)hotspot isNew:(BOOL)isNew
{
if (isNew)
    {
        [self.hotspots addObject:hotspot];
    }
    [self.tableViewHotspots reloadData];

    [self.navigationController popViewControllerAnimated:YES];
}

You will also make slight changes to the behavior of your view controller in order to handle the disappearing and re-appearing of your view by overriding the following two methods.

- (void)viewWillAppear:(BOOL)animated
{
    self.title = @"Hotspots";
    [super viewWillAppear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    self.title = @"Cancel";
    [super viewDidDisappear:animated];
}

Now to configure your UITableView, you must specify the number of rows to display, which will be based off of the count of your hotspots array.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.hotspots count];
}

You will configure the display of your table's cells to simply display the name and address of each Hotspot object.

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }

Hotspot *currentHotspot = [self.hotspots objectAtIndex:indexPath.row];

    cell.textLabel.text = currentHotspot.name;
    cell.detailTextLabel.text = currentHotspot.address;

return cell;
}

You will implement your table such that the selection of a row presents a HotspotInfoViewController with that row's information, so that the user can easily edit any given object.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
*)indexPath
{
    [self.tableViewHotspots deselectRowAtIndexPath:indexPath animated:YES];

    HotspotInfoViewController *hotspotVC = [[HotspotInfoViewController alloc] init];
    hotspotVC.delegate = self;
    hotspotVC.hotspot = [self.hotspots objectAtIndex:indexPath.row];
    [self.navigationController pushViewController:hotspotVC animated:YES];
}

Finally, you will also make your UITableView allow for both the rearranging and deletion of objects. To allow editing and deletion, you need to implement the following two methods.

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath
*)indexPath
{
return YES;
}

- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        [self.hotspots removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
    }
}

You must also override the -setEditing:animated: method to properly entwine your UITableView and your Edit button.

-(void)setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing:editing animated:animated];
    [self.tableViewHotspots setEditing:editing animated:animated];
}

To allow for the rearranging of cells, the following two methods will also be added.

- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath
*)indexPath
{
return YES;
}

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath
*)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
Hotspot *movingHotspot = [self.hotspots objectAtIndex:fromIndexPath.row];
    [self.hotspots removeObject:movingHotspot];
    [self.hotspots insertObject:movingHotspot atIndex:toIndexPath.row];
    [self.tableViewHotspots reloadData];
}

Finally, before you test your app, you will need to modify your application delegate's implementation file to put your view controller in a UINavigationController. Adjust your -application:didFinishLaunchingWithOptions: method to resemble the following.

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.viewController = [[MainViewController alloc]
initWithNibName:@"MainViewController" bundle:nil];
    __strong UINavigationController *navcon = [[UINavigationController alloc]
initWithRootViewController:self.viewController];
    self.window.rootViewController = navcon;
    [self.window makeKeyAndVisible];
    return YES;
}

Your user interface is now fully set up, allowing you to create Hotspot objects to be displayed, edited, or removed from a UITableView, as shown in Figure 10–8 after some sample data has been created.

Image

Figure 10–8. Your app’s UITableView with sample data

Now, the only task left to do with this application is to be able to save your data, persisting it between uses.

In order to implement persistence in your application, you will make use of the file system, as well as the concepts of “archiving” and “unarchiving” objects.

In order to archive, or “encode” any given object, it, and all the properties stored within it, must be specifically told how to be encoded. For any pre-made iOS object, such as an NSArray or NSDictionary this is already done. However, in order to encode your Hotspot objects, you will need to add some specific instructions on how they are to be handled.

In your Hotspot.h class, specify that your class will conform to the NSCoding protocol by changing your @interface line to the following:

@interface Hotspot : NSObject<NSCoding>

Now, you must implement the -encodeWithCoder: method to specify how a Hotspot object is coded for saving.

- (void) encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeObject:self.address forKey:@"address"];
    [encoder encodeObject:self.city forKey:@"city"];
    [encoder encodeObject:self.state forKey:@"state"];
}

In the reverse process of loading data, you must implement the -initWithCoder: method to create instances of the Hotspot class using archived data. By using the same keys as in the preceding method, you can easily pull out the NSString objects that you need.

- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self)
    {
self.name = [decoder decodeObjectForKey:@"name"];
self.address = [decoder decodeObjectForKey:@"address"];
self.city = [decoder decodeObjectForKey:@"city"];
self.state = [decoder decodeObjectForKey:@"state"];
    }
return self;
}

Now, back in your main view controller, you can create a method to save your data to a specific file.

-(void)saveData
{
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES) objectAtIndex:0];
NSString *savePath = [rootPath stringByAppendingPathComponent:@"hotspotsData"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSMutableData *saveData = [[NSMutableData alloc] init];

NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:saveData];
    [archiver encodeObject:self.hotspots forKey:@"DataArray"];
    [archiver finishEncoding];

    [fileManager createFileAtPath:savePath contents:saveData attributes:nil];
}

This method consists of the following steps:

  1. Acquire the root directory path in which you will save your data. You have specified the “Documents Directory”, though there are other possible directories to use depending on your application’s needs.
  2. Append a file name “hotspotsData” onto the root path to create the data file’s path.
  3. Acquire a shared instance of the NSFileManager class.
  4. Acquire an empty instance of NSMutableData.
  5. Encode your hotspots NSArray with the key “DataArray” using the NSKeyedArchiver class. The resulting encoded data will be in the NSMutableData saveData object you acquired.
    1. Though you cannot see the actual code of it, the -encodeObject:forKey: call will make use of the -encodeWithCoder: method you defined in your Hotspot.m file to archive all the Hotspot objects in your array.
  6. Using the NSFileManager, create your file at the specified path, using your encoded data. If a file already exists at this path, it will be overwritten, which works quite well for your application.

Now, to work in the reverse process, you can create a -loadData method like so:

-(void)loadData
{
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES) objectAtIndex:0];
NSString *savePath = [rootPath stringByAppendingPathComponent:@"hotspotsData"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:savePath])
    {
NSData *data = [fileManager contentsAtPath:savePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
self.hotspots = [unarchiver decodeObjectForKey:@"DataArray"];
    }
}

This method works almost the same as the previous one in reverse, as it acquires NSData from a specific file path, unarchives it, and then creates your NSArray back from the decoded data. Just as the encodeObject:forKey: method made use of your -encodeWithCoder:, the decodeObjectForKey: method will make use of the -initWithCoder: method for all the objects involved in your decoded object, including your NSArray and all the Hotspots it contains.

Now, you simply need to place calls to your two new methods in the correct places. Add a call to -loadData to your -viewDidLoad method.

[self loadData];

Add method signatures for both the -saveData and -loadData methods to your view controller’s header file to avoid any compiler problems.

You will need to add calls to the -saveData method every time your array is altered, including in the following methods:

-(void)HotspotInfoViewController:(HotspotInfoViewController *)hotspotInfoVC
didReturnHotspot:(Hotspot *)hotspot isNew:(BOOL)isNew
{
    if (isNew)
    {
        [self.hotspots addObject:hotspot];
    }
    [self.tableViewHotspots reloadData];
    //New call to save data
    [self saveData];

    [self.navigationController popViewControllerAnimated:YES];
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        [self.hotspots removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
        //New call to save data
        [self saveData];
    }
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath
*)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
    Hotspot *movingHotspot = [self.hotspots objectAtIndex:fromIndexPath.row];
    [self.hotspots removeObject:movingHotspot];
    [self.hotspots insertObject:movingHotspot atIndex:toIndexPath.row];
    [self.tableViewHotspots reloadData];
    //New call to save data
    [self saveData];
}

Once these calls have been added, your application will be able to automatically save any new changes to your data to an outside file, to be read upon the reopening of the application!

In your demo application, you did not implement any saving of images to file, even though this is one of the most effective and common uses of the iOS file management system. This process is even simpler than the foregoing implementation. In order to save an image, acquire the NSData object representing the image, and then use the NSFileManager method -createFileAtPath:contents:attributes to write out the data. Alternatively, you can use the NSData method -writeToFile:atomically: to perform the same task.

To acquire the data representing a UIImage, you can make use of the UIImagePNGRepresentation() or UIImageJPEGRepresentation() functions, which both return NSData. To re-build a UIImage using NSData read from a file, use the +imageWithData: or -initWithData: methods.

Core Data

So far you have dealt with the very quick implementation of NSUserDefaults for lightweight values, as well as the file management systemfor more complex or larger amounts of data. While using the file management system is incredibly powerful for storing data, it can easily become quite cumbersome when dealing with complex data models of intertwined classes. For such cases of complex data models made up of lightweight objects, the best option for persistence becomes Core Data. This framework, based around a MySQL table system, allows for easy creation, manipulation, and persistence of intricate class interactions and properties. The use of this class is quite complex, and thus is covered in great detail in the next chapter.

Recipe 10–3: Persistence with iCloud

One of the most expansive additions to iOS with the release of iOS 5.0 is the ability to create applications that have access to the new iCloud service. By using concepts similar to the file management system from the previous recipe, you are able to save, persist, and load data, not just in a local device, but also across multiple devices using the same application.

Throughout this section, all recipes will require access to an iOS development program, as well as a physical iOS device.

In order to configure an application for use with iCloud, a variety of configuration tasks must first be completed. Start by creating a new project using the Single View Application template as before. Make sure your project name is “iCloudTest”, and your class prefix is “iCloudStore”. While these values normally do not make much difference in your applications, it will help simplify this demonstration if your names follow those used in this recipe. Your project configuration should resemble Figure 10–9.

Image

Figure 10–9. Configuring an application to work with iCloud

First, you must configure your project to allow for “entitlements.” In your new project, navigate to the project’s Target settings, and scroll down to the bottom section called “Entitlements”. Click the check box labeled “Enable Entitlements”, as shown in the bottom of Figure 10–10, to have Xcode automatically generate your entitlements file.

Image

Figure 10–10. Enabling entitlements to allow communication with iCloud

Next, you need to generate a special “App ID” for this application. In your web browser, log into the iOS Developer’s Member Center at http://developer.apple.com/membercenter/. Navigate to the iOS Provisioning Portal, and then move to the App IDs section. Click the button titled “New App ID”.

The next screen you see will prompt you to enter a description, as well as a bundle identifier. Set the description to “iCloudTest”. For the bundle identifier, you need to enter the exact same identifier that Xcode has given your app. This can be found at the top of the project’s Targets settings, above the Entitlements section, as in Figure 10–11. It will most likely have a format along the lines of “com.domainName.iCloudTest”. Copy the text listed under “Identifier” into the Bundle Identifier line in your browser.

Image

Figure 10–11. Finding the identifier for your app to configure

In Figure 10–11, my identifier is “com.ColinFrancis.iCloudTest”, so I will copy this to my browser as the bundle identifier, as in Figure 10–12.

Image

Figure 10–12. Copying your project's identifier into the Bundle Identifier field

Upon creating this new App ID, you will be returned to your table of created App IDs. Find the one that you just created, and click the Configure link.

In this screen, all you need to do is check the box labeled “Enable for iCloud”, shown in Figure 10–13. If a dialog appears warning you of having to manually regenerate profiles, simply click OK.

Image

Figure 10–13. Enabling iCloud for your certificate

Click Done to finish configuring your App ID.

Next, move down to the Provisioning tab listed on the left-hand side of the screen underneath the App IDs tab. Click on the New Profile button to begin creating a new provisioning profile.

Name this new profile “iCloudProfile”. Select your certificate that you should already have as an iOS developer. Set the App ID field to your recently made “iCloudTest” App ID, and make sure to check the boxes next to whichever devices you want to test this application on. Figure 10–14 shows my configuration screen, which yours should resemble with your own information.

Image

Figure 10–14. Creating a new provisioning profile for your iCloud app

Click Submit to return to your list of provisioning profiles. You should see your new profile listed. If its status is listed as “Pending”, simply refresh the page until it says “Active”.

Next, click the Download button next to your newly created profile to download it to your computer.

Once your file has finished downloading (it shouldn’t take long), drag the file from the Finder to the Xcode icon in your dock to import it into Xcode. This should bring up the Organizer window as well, which will list all of your provisioning profiles.

Finally, in the Organizer, while your device is connected to your computer, drag the new profile from the displayed list to the Provisioning Profiles section under your device, shown in Figure 10–15.

Image

Figure 10–15. Copying your new profile into the Provisioning Profiles section of your device

At this point, your device is fully configured to run the project you have created.

You must perform one last step in your project in order for the application to physically be able to use the iCloud services. You will need to define a constant in your view controller with the “Ubiquity Container URL”. This NSString will essentially be your developer account’s ID prefixed to your bundle identifier. For example, if you have your bundle identifier “com.domainName.iCloudTest” and account ID “12345ABCDE”, the URL will be “12345ABCDE.com.domainName.iCloudTest”. If you are unsure of your account ID, you can find it by navigating to the Member Center again and going to the Your Account tab. If you are using an individual account, it will be listed under your name next to “Individual ID”.

Throughout this project, you will use the example URL of “12345ABCDE.com.domainName.iCloudTest”. Make sure that you change this according to your own account ID and domain name.

Add the following definition to the top of your iCloudStoreViewController.m file so that you can reference it later.

#define UBIQUITY_CONTAINER_URL @"12345ABCDE.com.domainName.iCloudTest"

In order for your application to work properly, you must also make sure that your entitlements file has been properly configured with this same URL. Navigate to your entitlements file, and make sure the values for both com.apple.developer.ubiquity-container-identifiers (Item 0) and com.apple.developer.ubiquity-kvstore-identifier are set to this value. Xcode should have automatically set these, but it is best to always confirm this setup. If not, set them, as shown in Figure 10–16.

Image

Figure 10–16. Confirming the correct configuration of your entitlements file

Now that your application and device are configured to work with iCloud, you can continue to build your actual application.

In order to build a simple program to save a text document to iCloud, you need to make use of the UIDocument abstract class. Your subclass of this will contain all the information needed to encode your text information into a file that can be saved online. Create a new file and select the “Objective-C class” template. The easiest way to configure this file is to start off by naming the NSObject class as the parent class. You will change the actual superclass shortly. Name the file “MyDocument”.

Once your class has been created, modify the @interface line of the MyDocument.h file to the following in order to specify your class as a subclass of UIDocument.

@interface MyDocument : UIDocument

You will give this class only one property, userText, of type NSString, which will store the text to be encoded and decoded. Make sure to synthesize this property in the implementation file. Your MyDocument.h file should look like so:

#import <Foundation/Foundation.h>

@interface MyDocument : UIDocument

@property (strong, nonatomic) NSString *userText;

@end

Now, you must, at a bare minimum, implement two classes to correctly subclass UIDocument: -contentsForType:error: and -loadFromContents:ofType:error:. These methods will essentially act as your encoding and decoding methods respectively, similar to the previous recipe.

Your first method will return an NSData object representing your userText property.

-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
return [NSData dataWithBytes:[self.userText UTF8String] length:[self.userText length]];
}

Your second method will do the reverse, building an NSString out of raw data and setting it to your property.

-(BOOL) loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError
*__autoreleasing *)outError
{
if ([contents length] >0)
    {
self.userText = [[NSString alloc] initWithBytes:[contents bytes] length:[contents
length] encoding:NSUTF8StringEncoding];
    }
else
    {
self.userText = @"";
    }
return YES;
}

Now that your data model is configured (yes, it is that simple!), you can move on to building your user interface. Switch over to the iCloudStoreViewController.xib file and, using a UITextView and a UIButton, set up the view shown in Figure 10–17.

Image

Figure 10–17. A simple user interface to save text

Make sure to leave the lower half of the view entirely blank, as the keyboard used to edit your UITextView will cover this area.

You may also wish to change some of the background colors of your main UIView and UITextView to clearly separate them. I made them different shades of gray, as shown later in Figure 10–19.

Connect the UITextView to your view controller’s header file using the textViewDisplay property. Make sure to synthesize and properly handle this property as usual. Connect the UIButton to an IBAction with the handler -savePressed:.

You will also need several other properties to help perform your iCloud operations. Add an import statement for the MyDocument.h file.

#import "MyDocument.h"

Add the following three properties as well, making sure to synthesize each and nil them in -viewDidUnload as usual.

@property (strong, nonatomic) MyDocument *document;
@property (strong, nonatomic) NSURL *ubiquityURL;
@property (strong, nonatomic) NSMetadataQuery *metadataQuery;

Next, you will implement your -viewDidLoad method to create a query for iCloud to search for any saved versions of your document by using the NSFileManager and NSMetadataQuery classes.

- (void)viewDidLoad
{
    [super viewDidLoad];

NSFileManager *filemgr = [NSFileManager defaultManager];

self.ubiquityURL = [[filemgr URLForUbiquityContainerIdentifier:UBIQUITY_CONTAINER_URL]
URLByAppendingPathComponent:@"Documents"];

if (self.ubiquityURL != nil)
    {
if ([filemgr fileExistsAtPath:[self.ubiquityURLpath]] == NO)
            [filemgr createDirectoryAtURL:self.ubiquityURL
withIntermediateDirectories:YES
attributes:nil
error:nil];

self.ubiquityURL = [self.ubiquityURL URLByAppendingPathComponent:@"document.doc"];

self.metadataQuery = [[NSMetadataQuery alloc] init];
        [self.metadataQuery setPredicate:[NSPredicate
predicateWithFormat:@"%K like 'document.doc'",
NSMetadataItemFSNameKey]];
        [self.metadataQuery setSearchScopes:[NSArray
arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope,nil]];

        [[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(metadataQueryDidFinishGathering:)
name: NSMetadataQueryDidFinishGatheringNotification
object:metadataQuery];
        [self.metadataQuery startQuery];
    }
}

This method contains the following steps to create a query for any saved data:

  1. Acquire the ubiquityURL using the -URLForUbiquityContainerIdentifier: method. This call uses your identifier that you defined earlier based on your account ID and domain name. If this value is not nil, then your device and application are correctly configured to store documents in iCloud.
  2. Ensure that the property directories exist at the target URL by using the -createDirectoryAtURL:withIntermediateDirectories:attributes:error: method.
  3. Append the file name “document.doc” onto the full URL.
  4. Create an instance of NSMetadataQuery with a predicate for your file name and a search scope for ubiquitous documents.
  5. Add the view controller as an observer for the completion of the query.
  6. Start the query.

After calling this method, your application will be off attempting to find any information stored on iCloud. In order to react to its results, you must implement the -metadataQueryDidFinishGathering: selector you mentioned in the previous notification registration.

- (void)metadataQueryDidFinishGathering: (NSNotification *)notification
{
NSMetadataQuery *query = [notification object];
    [query disableUpdates];

    [[NSNotificationCenter defaultCenter]
removeObserver:self
name:NSMetadataQueryDidFinishGatheringNotification
object:query];

    [query stopQuery];
NSArray *results = [[NSArray alloc] initWithArray:[query results]];

if ([results count] == 1)
    {
self.ubiquityURL = [[results lastObject] valueForAttribute:NSMetadataItemURLKey];
self.document = [[MyDocument alloc] initWithFileURL:ubiquityURL];

        [self.document openWithCompletionHandler:^(BOOL success)
        {
if (success)
             {
NSLog(@"Opened iCloud doc");
self.textViewDisplay.text = self.document.userText;
             }
else {
NSLog(@"Failed to open iCloud doc");
   }
         }];
    }
else
    {
self.document = [[MyDocument alloc] initWithFileURL:self.ubiquityURL];
        [self.document saveToURL:self.ubiquityURL forSaveOperation:
UIDocumentSaveForCreating completionHandler:^(BOOL success)
        {
if (success)
              {
NSLog(@"File created and saved to iCloud");
              }
else
              {
NSLog(@"Error, could not save file to iCloud");
              }
        }];
    }
}

This method completes your search for any previously stored information in iCloud by running through the following steps:

  1. Retrieves the original NSMetadataQuery object in order to disable any further updates, remove the view controller as an observer, and stop the query.
  2. Create an NSArray of documents found using the results property of NSMetadataQuery.
  3. Acquire the last/only object in this array, and then create a UIDocument using its key-valued URL.
  4. Open the document, and, upon completion, display the document’s text to the user.

This method also includes code for the case in which no documents (or more than one) are found. In this case, the program attempts to save an empty file directly to iCloud, so that the file will exist in the future.

At this point, your application will be able to load any previously stored text from iCloud, so you simply need to implement a saving functionality to have any information to retrieve! The process of saving data is much simpler than retrieving it.

- (void)savePressed:(id)sender
{
self.document.userText = self.textViewDisplay.text;
    [self.document saveToURL:self.ubiquityURL
forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success)
    {
if (success)
        {
NSLog(@"Written to iCloud");
        }
else
        {
NSLog(@"Error writing to iCloud");
        }
    }];
}

Before you continue on to test your application, you must make sure that your test device is properly configured to work with iCloud. In the Settings app on your device, navigate to the iCloud section. In order for this application to properly store data, your iCloud account must be properly configured and verified. This will require you to have verified your e-mail address and registered it as your Apple ID. The item marked “Documents & Data” should also be set to “On”, as in Figure 10–18. You can, of course, easily configure this once your account is verified.

Image

Figure 10–18. Documents and Data must be enabled to store information in iCloud.

Assuming your device is correctly configured, your simple application should be able to correctly store documents using the user’s iCloud account, allowing you to easily persist data across multiple devices, application shutdowns, and even through system resets, as shown by Figure 10–19 with a few slightly altered background colors, as mentioned earlier.

Image

Figure 10–19. Your application with text saved and loaded from iCloud

Recipe 10–4: Storing Key-Value Data in iCloud

Just as you were able to easily persist a variety of lightweight objects locally using the NSUserDefaults class at the beginning of this chapter, you are able to implement the same type of storage using the iCloud service. You will add this functionality to your application by keeping a count of the number of times that the text has been saved. While this may not seem like much, the ability to have such simple values remain synchronized in the same application across multiple devices opens up a whole world of development power.

All key-value data handling with iCloud is done through the NSUbiquitousKeyValueStore class. You will add an instance of this class as a property to your iCloudStoreViewController file, making sure to synthesize and nil it as appropriate.

@property (strong, nonatomic) NSUbiquitousKeyValueStore *keyStore;

Next, you will do a slight rearrangement of your user interface to include two UILabels, of which one will display your save count. Your view will now resemble Figure 10–20.

Image

Figure 10–20. Rearranging your XIB file with two new UILabels

Connect the right UILabel (displaying the text “N” in Figure 10–20) to your header using the property countLabel.

@property (strong, nonatomic) IBOutlet UILabel *countLabel;

You can configure your application to immediately check for any count upon loading by adding the following code to the end of your -viewDidLoad method.

self.keyStore = [[NSUbiquitousKeyValueStore alloc] init];
double count = [self.keyStore doubleForKey:@"count"];
self.countLabel.text = [NSString stringWithFormat:@"%f", count];

The following line, added after the foregoing ones, will allow you to receive notifications upon the changing of any values in your NSUbiquitousKeyValueStore, allowing you to keep your information updated across multiple devices.

[[NSNotificationCenter defaultCenter] addObserver:self selector:
@selector(countChangeExternally:) name:
NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:self.keyStore];

You can implement the -countChangeExternally: method quite simply to set your UILabel’s text.

-(void)countChangeExternally:(id)sender
{
double count = [self.keyStore doubleForKey:@"count"];
self.countLabel.text = [NSString stringWithFormat:@"%f", count];
}

Finally, you need to instruct your savePressed: method to correctly update this value. You will do this only if your text is successfully saved, so your updated method will look like so:

- (void)savePressed:(id)sender
{
self.document.userText = self.textViewDisplay.text;
    [self.document saveToURL:self.ubiquityURL
forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success)
    {
if (success)
        {
NSLog(@"Written to iCloud");
double count = [self.countLabel.text doubleValue];
            count += 1;
self.countLabel.text = [NSString stringWithFormat:@"%f", count];
            [self.keyStore setDouble:count forKey:@"count"];
            [self.keyStore synchronize];
        }
else
        {
NSLog(@"Error writing to iCloud");
        }
    }];
}

Your application should now be able to easily store your lightweight double value to keep track of your save count, as shown in Figure 10–21!

Image

Figure 10–21. The app with both text and key-value information stored through iCloud

Summary

Data persistence is almost always one of the most important considerations in developing an application. Developers must consider the type of data they wish to store, how much of it, how it connects, and whether their application might even stretch across multiple devices. From there, the choice must be made to decide which approach to use to storing data, whether it is the simple NSUserDefaults method, the powerful file management system, or the intricate Core Data framework (as discussed in the next chapter). In iOS 5.0, the new addition of access to the iCloud service has revolutionized the way that applications can store data, allowing persistence of data in near real time across devices running the same application. As memory, storage, and mobile applications continue to grow in size, importance, and relevance in the technological world, these topics will become even significantly more relevant. By firmly understanding the most up-to-date concepts of data persistence in iOS, you are able to always keep your users updated with the fastest, most efficient, and most powerful methods of storing data possible.

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

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