Chapter    12

Data Storage Recipes

When working in iOS, one of the most important topics to understand is the concept, use, and implementation of persistence. Implementation of persistence refers to the idea of having information saved and retrieved, or persist, through the closing or restarting of an application. Just as pages from books can be read and re-read, even after closing and re-opening them, you can make use of certain key concepts in iOS to allow your information—from the simplest of values to the most complex of data structures—to stay stored in your device for indefinite periods of time. We cover a variety of methods of persistence throughout this chapter with different advantages, disadvantages, general uses, and complexities, so that you can develop a full understanding of the best method of storage for any given situation.

Recipe 12-1: Persisting Data with NSUserDefaults

When developing applications, you often run into situations where you want to store simple values, for example user settings or some part of an app’s state. While there are a variety of ways to store data, the easiest of these is NSUserDefaults, built specifically for such simple situations.

The NSUserDefaults class is a simple API used to store basic values, such as instances of NSString, NSNumber, BOOL, and so on. 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; images, for example, should not be stored with NSUserDefaults.

In this recipe you’ll build a simple app that has a state that’ll persist using NSUserDefaults. Start off by creating a new single-view application project called “Stubborn” (because you want your information to stick around).

Set up the user interface of this app. Select the ViewController.xib file to bring up Interface Builder for the main view. Add two Text fields, a Switch and an Activity indicator and set them up so that the user interface resembles Figure 12-1.

9781430245995_Fig12-01.jpg

Figure 12-1.  A user interface whose state will be persisted

As you can probably guess, the Switch starts and stops the Activity indicator. You also write code that persists the state of the Switch along with the text you’ve entered in the Text fields.

First you need a way to reference the controls from your code, so create the following outlets:

  • firstNameTextField
  • lastNameTextField
  • activitySwitch
  • activityIndicator

You also need to intercept when the user taps the Switch, so create an action named toggleActivity for its Value Changed event.

In the ViewController.h file, add the UITextFieldDelegate protocol to the ViewController class. You need this to control the keyboard later. In all, the ViewController.h file should now resemble the code that follows:

//
//  ViewController.h
//  Stubborn
//
 
#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController<UITextFieldDelegate>
 

@property (weak, nonatomic) IBOutlet UITextField *firstNameTextField;
@property (weak, nonatomic) IBOutlet UITextField *lastNameTextField;
@property (weak, nonatomic) IBOutlet UISwitch *activitySwitch;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
 
- (IBAction)toggleActivity:(id)sender;
 
@end

You now start implementing the basic functionality of the controls, starting with the text fields. Open to the ViewController.m file and add the following code to the viewDidLoad method:

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

Now, add the following delegate method. It makes sure the keyboard gets removed if the user taps the Return button:

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

Now it’s time to implement the behavior of the Switch. Add the following implementation to the toggleActivity: action method:

- (IBAction)toggleActivity:(id)sender
{
    if (self.activitySwitch.on)
    {
        [self.activityIndicator startAnimating];
    }
    else
    {
        [self.activityIndicator stopAnimating];
    }
}

The simple user interface is now fully functioning and you should take it on a test spin. You should be able to enter text in the text fields and start and stop the activity indicator animation by tapping the switch. However, if you shut down the app and rerun it, the text will be gone and the switch will be back to its OFF state again. Let’s implement some persistency, shall we?

As you know, there are two things you need to do to persist data; you need to save it, and you need to restore it—at appropriate times. There are basically two strategies for when to save persisted data. You could either store the data whenever it’s changed, or you could save it right before the app terminates. In this recipe, you implement the second strategy and have the state saved when the app enters the background.

Note  Normally, an app that’s suspended is not killed but put to sleep and can be reactivated and brought back to the same state without the need for persisting its data. However, in case of low-memory conditions, an app can be terminated without warning. Because there is no way to know whether your app is being killed off, you should always be sure that your persisted data is saved when the app enters the background.

To know when the app enters the background mode you can use the Notification center and register an Observer of UIApplicationDidEnterBackgroundNotification. A good place to do this is when the view is loaded, so add the following code to viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view, typically from a nib.
    self.firstNameTextField.delegate = self;
    self.lastNameTextField.delegate = self;

     [[NSNotificationCenter defaultCenter] addObserver:self
         selector:@selector(savePersistentData:)
         name:UIApplicationDidEnterBackgroundNotification object:nil];
}

Now you can implement in the savePersistentData: method the actual storing of the persistent data, like so:

- (void)savePersistentData:(id)sender
{
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    //Set Objects/Values to Persist
    [userDefaults setObject:self.firstNameTextField.text forKey:@"firstName"];
    [userDefaults setObject:self.lastNameTextField.text forKey:@"lastName"];
    [userDefaults setBool:self.activitySwitch.on forKey:@"activityOn"];

    //Save Changes
    [userDefaults synchronize];
}

Tip  You can use NSUserDefault’s resetStandardUserDefaults method to clear all data that’s been previously stored. This can be a good way to reset your app to its standard settings.

What’s left now is to load the data when the app launches. Start by adding the following method to perform the loading:

- (void)loadPersistentData:(id)sender
{
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    self.firstNameTextField.text = [userDefaults objectForKey:@"firstName"];
    self.lastNameTextField.text = [userDefaults objectForKey:@"lastName"];
    [self.activitySwitch setOn:[userDefaults boolForKey:@"activityOn"] animated:NO];
 
    if (self.activitySwitch.on)
    {
        [self.activityIndicator startAnimating];
    }
}

And finally, call the loadPersistentData: method from the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view, typically from a nib.
    self.firstNameTextField.delegate = self;
    self.lastNameTextField.delegate = self;

    [self loadPersistentData:self];

    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(savePersistentData:)
        name:UIApplicationDidEnterBackgroundNotification object:nil];
}

You’re now done implementing the persistencey of the app’s state. Open the app, enter some text in the text fields and turn the activity switch to ON. Now press the Home button on the device to make the app enter the background mode. The data should now be saved to NSUserDefault, but to truly test whether that really happened, you need to kill the app before relaunching it. To do that you can either stop the app’s execution from Xcode, or you can double-press the Home button, locate the “Stubborn” app in the list of suspended apps; if you tap and hold its icon until a wiggling “−” minus sign appears, which you tap to terminate the app.

Now, if you rerun the app you’ll see that it appears just as you left it. Figure 12-2 shows an example of this app right after it has been relaunched.

9781430245995_Fig12-02.jpg

Figure 12-2.  An app that has restored its state from the previous run, using NSUserDefaults

Although 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.

Remember, though, NSUserDefaults is meant for relatively small amounts of data. In the next recipe we’ll show you how you can store somewhat bigger chunks, using files.

Recipe 12-2: Persisting Data Using 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 documents, videos, music, or images. For these more complex items, you can make use of iOS’s file management system.

In this recipe you’ll create a simple app that allows you to enter a long text and save it to a file. Start by creating a new single-view application project. You can name it “My Text Document Editor.”

Next, build a user interface that resembles Figure 12-3. You’ll need a Label, a Text Field, a Text View, and three Round Rect Buttons.

9781430245995_Fig12-03.jpg

Figure 12-3.  A simple app for editing, saving, and loading text files

Create the following outlets and actions for the respective components:

  • Outlets:filenameTextField and contentTextView
  • Actions:saveContent, loadContent, and clearContent

With the user interface in place you can start implementing its functionality. But first, create a helper method that transforms the relative filename into an absolute file path within the Documents directory of the device. Add the following method to the ViewController.m file:

- (NSString *)currentContentFilePath
{
    NSArray *documentDirectories =
        NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [documentDirectories objectAtIndex:0];
    return [documentsDirectory
        stringByAppendingPathComponent:self.filenameTextField.text];
}

Now, when the user taps the Save button, you’re going to save the content of the Text View to the file path provided by the helper method you just created. Add the following implementation to the saveContent: action method:

- (IBAction)saveContent:(id)sender
{
    NSString *filePath = [self currentContentFilePath];
    NSString *content = self.contentTextView.text;
    NSError *error;
    BOOL success = [content writeToFile:filePath atomically:YES
        encoding:NSUnicodeStringEncoding error:&error];
    if (!success)
    {
        NSLog(@"Unable to save file: %@ Error: %@", filePath, error);
    }
}

Conversely, when the user taps the Load button, you will load the content from the file and update the Text View. Here’s the implementation of the loadContent: action method:

- (IBAction)loadContent:(id)sender
{
    NSString *filePath = [self currentContentFilePath];
    NSError *error;
    NSString *content = [NSString stringWithContentsOfFile:filePath
        encoding:NSUnicodeStringEncoding error:&error];
    if (error)
    {
        NSLog(@"Unable to load file: %@ Error: %@", filePath, error);
    }
    self.contentTextView.text = content;
}

Finally, the Clear button simply clears the Text View, like so:

- (IBAction)clearContent:(id)sender
{
    self.contentTextView.text = nil;
}

You now have a very rudimentary text file editor, now try it out. Build and run the app. Enter some text in the Text View, enter a filename in the Filename text input, and hit the Save button. The app then creates a file in the Documents directory on the device (or on your disk if you’re running the app in the iOS Simulator). To verify that its been correctly saved, you can tap Clear to reset the Text View and then Load. The text you just wrote should now reappear in the Text View. You can also try to create different files by changing the contents of the Filename text field.

Although this app works, it has one serious problem which we’d like to address before leaving this recipe. If you save the content to an existing file, the app will silently overwrite its content, which may or may not be what the user wants. To make sure you catch the user’s intention, you’re going to check whether the file exists and ask for permissions to replace it if it does. You do this by changing the implementation of the saveContent: method. Start by extracting the actual saving into a helper method, called saveContentToFile:, like so:

- (void)saveContentToFile:(NSString *)filePath
{
    NSString *content = self.contentTextView.text;
    NSError *error;
    BOOL success = [content writeToFile:filePath atomically:YES
        encoding:NSUnicodeStringEncoding error:&error];
    if (!success)
    {
        NSLog(@"Unable to save file: %@ Error: %@", filePath, error);
    }
}

Next, make the following changes to the saveContent: method:

- (IBAction)saveContent:(id)sender
{
    NSString *filePath = [self currentContentFilePath];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:filePath])
    {
        UIAlertView *overwriteAlert = [[UIAlertView alloc] initWithTitle:@"File Exists"
            message:@"Do you want to replace the file?" delegate:self
            cancelButtonTitle:@"No" otherButtonTitles:@"Yes", nil];
        [overwriteAlert show];
    }
    else
        [self saveContentToFile:filePath];
}

Now, go to ViewController.h and add the UIAlertViewDelegate protocol so that the view controller can act as the Alert view’s delegate and intercept when the user taps its buttons:

//
//  ViewController.h
//  My Text Document Editor
//
 
#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController<UIAlertViewDelegate>
 

@property (weak, nonatomic) IBOutlet UITextField *filenameTextField;
@property (weak, nonatomic) IBOutlet UITextView *contentTextView;
 
- (IBAction)saveContent:(id)sender;
- (IBAction)loadContent:(id)sender;
- (IBAction)clearContent:(id)sender;
 
@end

Finally, back in ViewController.m, add the following delegate method for when the user taps one of the Alert view’s buttons:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
    {
        // User tapped Yes button, overwrite the file
        NSString *filePath = [self currentContentFilePath];
        [self saveContentToFile:filePath];
    }
}

Now you’re done and can run the app again. This time, if you try to save a file that already exists, you’ll be asked if you want to replace it (see Figure 12-4).

9781430245995_Fig12-04.jpg

Figure 12-4.  An app asking if the user’s intention was to overwrite an existing file

In this demo app, you’ve only worked with text data. However, other types of data are equally simple. NSImage, for example, has methods for saving and loading from files, as have most other common data types. And even if what you want to save doesn’t have direct file support, you can always convert it to an NSData object, which has.

While files are great for storing documents and isolated pieces of data, they are not very handy when it comes to persisting multiple objects with internal relationships, which is the natural data model of many apps. For these applications, a better alternative is to use the Core Data framework, the topic of the next recipe.

Recipe 12-3: Using Core Data

So far you have dealt with the quick implementation of NSUserDefaults for lightweight values, as well as the file management system for 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, the best option becomes Core Data.

In this recipe you build a simple word list app that persists its data using Core Data. But before you start, let’s quickly go through the basics of this framework.

Understanding Core Data

The Core Data framework is designed around the concept of relational data. However, it’s not a relational database, but rather a layer of abstraction on top of some storage entity, usually SQLite. With Core Data you can focus on the structure of your data and leave the low-level relational database details for the framework to handle.

Put simply, Core Data, in conjunction with Xcode, allows a developer to perform three main tasks:

  1. Create a data model
  2. Persist information
  3. Access data

First, it is important to understand exactly what a data model is. This term applies essentially to whatever structure any given application’s data is built around. This could be something as simple as an NSString or an NSArray in a simple application, all the way up to a complex, interconnected system of object types, each with their own properties, methods, and pointers to other objects.

Core Data is one of the most powerful frameworks in iOS. Despite this, its API is surprisingly small, consisting of only a handful of classes for you to handle. Here are some brief descriptions of the few main classes that makes up Core Data:

  • NSManagedObjectModel: This object is how iOS refers to your data model, but you will have little to do with this class yourself. When you create your project for the first recipe, you will see an instance of this type in your application delegate, and you will see it used in some pre-generated methods, but aside from that, you will have no reason to deal with this class programmatically.
  • NSPersistentStoreCoordinator: This class, too, is one that you very rarely will need to deal with. It works mostly in the background of an application to “coordinate” between your application and the underlying database or “Persistent Store,” but you will not need to send any actions to it. The most important part of this class that you need to know about is the “type” of persistent store that is being used. There are three types of persistent stores:
  •  NSSQLiteStoreType
  •  NSBinaryStoreType
  •  NSInMemoryStoreType

    The default value is NSSQLiteStoreType, specifying that you are using a persistent store built around the SQLite foundation. You will continue to use this typefor the purpose of the Core Data recipes in this chapter.

  • NSManagedObjectContext: This class, unlike the previous two, is one that you will be dealing with quite often. In the simplest terms, this class acts as a sort of “workspace” for your information. Any time you need to retrieve or store information, you will need a pointer to this class to perform the action. For this reason, a very common practice in Core Data–based applications is to “pass around” a pointer to this class between each part of the application by giving each view controller an NSManagedObjectContext property
  • NSManagedObject: This class represents an instance of actual data in the data model.
  • NSFetchedResultsController: This is the primary class for “fetching” results through the NSManagedObjectContext. It is not only very powerful, but also very easy to use, especially in conjunction with a UITableView. You will see plenty of examples of using this class in the recipes to come.

Now, let’s start building the word list app.

Setting Up Core Data

The easiest way to setup Core Data for your app is to let Xcode generate the necessary code when you create the project. Create a new project called “My Vocabularies using the Empty Application template, as shown in Figure 12-5.

9781430245995_Fig12-05.jpg

Figure 12-5.  Creating an empty application to start from scratch using the Empty Application template

On the next screen, where you enter the project name, be sure to select the box labeled Use Core Data (see Figure 12-6).

9781430245995_Fig12-06.jpg

Figure 12-6.  Checking the Use Core Data option makes Xcode set up Core Data for the application

After clicking Next, click Create on the next dialog box to finish the creation of your project as usual.

Now that you have set up your project to use Core Data, you have a lot of the work involved in using the Core Data framework already done for you, so you can move directly on to building your data model.

Designing the Data Model

For this app, you build a simple data model consisting of only Vocabularies and Words. However, before you proceed to do anything in Xcode, you need to plan out exactly how your model will work.

When working with a data model, the first kind of item you have to make is an “entity.” An entity is essentially the Core Data equivalent of a class, representing a specific object type that will be stored in the model.

In the same way that objects (or NSObjects in Objective-C) have properties, entities have “attributes.” These are the simpler pieces of data associated with any given entity, such as a name, age, or birthday, that do not require a pointer to any other entity.

Whenever you want one entity to have a pointer to another, you use a “relationship.” A relationship can be either “to-one” or “to-many,” referring to whether an entity has a pointer to one instance of another entity or multiple ones.

When dealing with the “to-many” relationship, you will notice that the entity has a pointer to a set of multiple other entities. Entities can easily have relationships that point to themselves, which might be the case of a Person entity having a relationship to another Person, in the form of a spouse. You can also set up “inverse relationships,” which act as paths back and forth between entities. For example, a Teacher entity might have a “to-many” relationship to a Student entity called “students,” and the Student’s relationship to the Teacher, called “teachers,” will be the inverse of this. Figure 12-7 shows a diagram of this two-way relationship.

9781430245995_Fig12-07.jpg

Figure 12-7.  Two entities with a to-many relationship pointing to one another

So for your data model, you have two entities with their respective attributes and relationships as defined in Table 12-1.

Table 12-1. The Data Model of the “My Vocabularies” app

Entity Attributes Relationships
Vocabulary name words
Word word, translation vocabulary

Note  By convention, pluralized relationship names indicate a to-many relationship, while singular names is used for to-one relationships.

Now that you have the data model planned out, you can build this in Xcode. Switch over to view your data model file, which is named My_Vocabularies.xcdatamodeld in your project. Your view should now resemble Figure 12-8.

9781430245995_Fig12-08.jpg

Figure 12-8.  The Data model editor with an empty data model

Now, add the two entities of your data model. You can do this from either the Editor menu, or by using the Add Entity button located in the bottom-center area of the Xcode window. When you add an Entity, you immediately can change the name of the entity, so enter “Vocabulary” and hit Return. Repeat the process for the “Word” entity.

Note  It’s easier to create all your entities first before trying to configure them, otherwise you won’t be able to set up the relationships.

After you’ve added the two entities, the list of Entities should resemble Figure 12-9.

9781430245995_Fig12-09.jpg

Figure 12-9.  The two entities of the “My Vocabulary” app data model

Start by configuring the Vocabulary entity, so be sure the “Vocabulary” text is selected in the ENTITIES section. By using the + button in the Attributes section, add an attribute called name with the type String selected in the Type drop-down menu, as in Figure 12-10.

9781430245995_Fig12-10.jpg

Figure 12-10.  An entity, Vocabulary, with a single attribute, name

Now you define the relationship of the Vocabulary entity. Under the Relationships area, add a relationship using the + button in that section. Name the relationship words. As the relationship’s Destination, assign the Word entity. Until you create the relationship in the other entity, you cannot set up the “Inverse” relationship, so leave it at “No Inverse.” The relationships setup for the Vocabulary entity should at this point be as in Figure 12-11.

9781430245995_Fig12-11.jpg

Figure 12-11.  Configuring the Vocabulary entity’s relationships

Now you’re going to define this relationship as a to-many relationship. You do that by selecting one of the relationships and check the “To-Many Relationship” option in the Data Model inspector (which corresponds to the Attribute inspector for view elements), as shown in Figure 12-12.

9781430245995_Fig12-12.jpg

Figure 12-12.  Defining a “to-many” relationship in the Data Model inspector

While you will most likely need not worry about most of the other values in this inspector (at least for the purposes of this recipe), one of the values of higher importance is the Delete Rule drop-down menu. This value specifies exactly how this relationship is handled when an instance of the given entity is deleted from the NSManagedObjectContext. It has four possible values:

  • No Action: This is probably the most dangerous value, as it simply allows related objects to continue to attempt to access the deleted object.
  • Nullify: The default value, this specifies that the relationship will be nullified upon deletion, and will thus return a nil value.
  • Cascade: This value can be slightly dangerous to use, as it specifies that if one object is deleted, all the objects it is related to via this Delete Rule will also be deleted, so as to avoid having nil values. If you’re not careful with this, you can delete unexpectedly large amounts of data, though it can also be very good for keeping your data clean. You may use this, for example, in the case of a “folder” with multiple objects. When a folder is deleted, you would want to delete all the contained objects as well.
  • Deny: This prevents the object from being deleted as long as the relationship does not point to nil.

Keep the Delete Rule on “Nullify” for this recipe.

Now, the turn has come to configure the Word entity. In the same way you did for the Vocabulary entity, select the “Word” text in the ENTITIES section, then move to the Attributes section and add two attributes this time, named word and translation. Use the type “String” for both of these attributes as well.

Also, add a relationship named vocabulary with the Destination set to Vocabulary. You can now also set the “Inverse” relationship to words, as shown in Figure 12-13. This automatically sets up the inverse relationship for the words relationship as well (to vocabulary).

9781430245995_Fig12-13.jpg

Figure 12-13.  Configuring a relationship with an inverse

Note  Inverse relationships are not always required, though they tend to make the organization and flow of your application a little bit better, allowing you to more easily access any piece of data you need from any other piece of data.

Because the vocabulary relationship is a to-one relationship (a Word can only belong to one Vocabulary), you should not select the “to-many” option as you did with the words relationship.

As the final step in the process of creating the data model, you create Objective-C classes that map to the respective entity. Be sure the Vocabulary entity is selected, go to the Editor menu and choose Create NSManagedObject Subclass…, and then click the Create button. This adds a new class to the project named Vocabulary, which you use later to access Vocabulary data in the data model. Repeat the process for the Word entity to create a corresponding Word class.

This is actually all you need to do to create your data model. To get a graphic overview of the data model, change the Editor Style to Graph in the lower-right corner of the Data Model editor. The Graph Editor style uses a UML notation to display the entities, their attributes and relationships, where a single arrow represents a “to-one” relationship, and a double arrow represents a “to-many” relationship. The blocks may initially appear all stacked on top of each other, but if you drag them apart, your display should resemble Figure 12-14.

9781430245995_Fig12-14.jpg

Figure 12-14.  A data model shown in the Graph Editor Style mode

Now that you have your data model set up, you can start to build the user interface to display its data.

Setting Up the Vocabularies Table View

Next you set up a navigation-based app with a main table view displaying a list of Vocabularies.

To start implementing this, add a new class to the project. Name the class VocabulariesViewController and make it a subclass of UITableViewController. You do not need an .xib file so leave that option unchecked.

Note  The UITableViewController class automatically sets up a table view and hooks up the necessary delegate properties. It’s a convenient way to quickly set up a table view controller in an application.

Now, make the following changes to the VocabulariesViewController.h file:

//
//  VocabulariesViewController.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "Vocabulary.h"
 
@interface VocabulariesViewController : UITableViewController<UIAlertViewDelegate>

 
@property (strong, nonatomic)NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic)NSFetchedResultsController *fetchedResultsController;
 
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context;
 
@end

What’s worth mentioning from the preceding code is that the fetchedResultsController property keeps track of the fetched data and the managedObjectContext property allows you to make any necessary requests for data. You may also be wondering why you make the view controller conform to the UIAlertViewDelegate protocol. The reason is that you use an alert view as an input dialog for the Vocabulary name later.

Now, switch to the VocabulariesViewController.m file to start implementing the view controller. Begin with the implementation for the custom initializer method:

- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context
{
    self = [super initWithStyle:UITableViewStylePlain];
    if (self)
    {
        self.managedObjectContext = context;
    }
    return self;
}

Next, add the following helper method, which fetches all Vocabularies in the data model and stores them in the fetchedResultsController property:

-(void)fetchVocabularies
{
    NSFetchRequest *fetchRequest =
        [NSFetchRequest fetchRequestWithEntityName:@"Vocabulary"];
    NSString *cacheName = [@"Vocabulary" stringByAppendingString:@"Cache"];
    NSSortDescriptor *sortDescriptor =
        [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
    [fetchRequest setSortDescriptors:@[sortDescriptor]];
    self.fetchedResultsController = [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext
        sectionNameKeyPath:nil cacheName:cacheName];
    NSError *error;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Fetch failed: %@", error);
    }
}

In detail, the previous method does the following:

  1. The first thing you need for fetching data is an instance of the NSFetchRequest class. Here, you have used a designated initializer to specify an NSEntityDescription, though you can also add it later using the -setEntity: method.
  2. While not required, you have set up a “cache name” to be used with your fetch request, with a different cache for each entity. This allows you to slightly improve the speed of your application if you are making frequent fetch requests, as a local cache is first checked to see if the request has already been performed.
  3. Every instance of NSFetchRequest is required to have at least one NSSortDescriptor associated with it. Here, you have specified a very simple alphabetic sort of the name property for each of your entities. After all your NSSortDescriptors have been created, they must be attached to the NSFetchRequest using the setSortDescriptors: method.
  4. After the NSFetchRequest is fully configured, you can initialize the NSFetchedResultsController using the NSFetchRequest and the NSManagedObjectContext. The last two parameters are both optional, though you have specified a cacheName for optimization. You can set both of these to nil if you want to ignore them.
  5. Finally, you must use the performFetch: method to complete the fetch request and retrieve the stored data. With this method, you can pass a pointer to an NSError, as shown previously, to keep track of and log any errors that occur with a fetch.

In the viewDidLoad method you initialize the view controller by setting its title and load the Vocabularies, like so:

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

To avoid presenting an empty list the first time the app is run, you’ll preload the data model with a “Spanish” Vocabulary, but only if no Vocabularies exist. To do that, add the following code to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    self.title = @"Vocabularies";
    [self fetchVocabularies];
    // Preload with a "Spanish" Vocabulary if empty
    if (self.fetchedResultsController.fetchedObjects.count == 0)
    {
        NSEntityDescription *vocabularyEntityDescription =
            [NSEntityDescription entityForName:@"Vocabulary"
                inManagedObjectContext:self.managedObjectContext];
        Vocabulary *spanishVocabulary = (Vocabulary *)[[NSManagedObject alloc]
            initWithEntity:vocabularyEntityDescription
            insertIntoManagedObjectContext:self.managedObjectContext];
        spanishVocabulary.name = @"Spanish";
        NSError *error;
        if (![self.managedObjectContext save:&error])
        {
            NSLog(@"Error saving context: %@", error);
        }
        [self fetchVocabularies];
    }
}

Next, you need to implement the required delegate and data source methods for the table view. First, the methods to specify the number of sections and rows:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.fetchedResultsController.fetchedObjects.count;
}

As shown, the NSFetchedResultsController class contains a method fetchedObjects, which returns an NSArray of the objects that were queried for.

Here is the method to configure the cells of the table view:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"VocabularyCell";
    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
            reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }
    Vocabulary *vocabulary = (Vocabulary *)[self.fetchedResultsController
        objectAtIndexPath:indexPath];
    cell.textLabel.text = vocabulary.name;
    cell.detailTextLabel.text =
        [NSString stringWithFormat:@"(%d)", vocabulary.words.count];
    return cell;
}

The basic setup of the main view controller is now finished and the time has come to wire it up. Go to the AppDelegate.h file and add the following declarations:

//
//  AppDelegate.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "VocabulariesViewController.h"
 
@interface AppDelegate : UIResponder <UIApplicationDelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator
    *persistentStoreCoordinator;
@property (strong, nonatomic) UINavigationController *navigationController;
@property (strong, nonatomic) VocabulariesViewController *vocabulariesViewController;
 
- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;
 
@end

As you can see from the preceding code, the application is the place where Core Data has been set up for you. All you need to do is to distribute the managed object context to the parts of your app that deal with the data.

Now, in the application:didFinishLaunchingWithOptions: method in AppDelegate.m, add the following code to create and display the view controller in a navigation controller:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
 
    self.vocabulariesViewController = [[VocabulariesViewController alloc]
        initWithManagedObjectContext:self.managedObjectContext];
    self.navigationController = [[UINavigationController alloc]
        initWithRootViewController:self.vocabulariesViewController];
    self.window.rootViewController = self.navigationController;
 
    [self.window makeKeyAndVisible];
    return YES;
}

Now is a good time to build and run the app to make sure everything is set up correctly. If things went right, you should see a screen resembling Figure 12-15.

9781430245995_Fig12-15.jpg

Figure 12-15.  A word list app with a single Vocabulary

To allow the user to add some data in the form of new Vocabularies, put an Add button on the Navigation bar. Go back to the viewDidLoad method in VocabulariesViewController.m and add the following code:

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

    UIBarButtonItem *addButton =
        [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
            target:self action:@selector(add)];
    self.navigationItem.rightBarButtonItem = addButton;
 
    [self fetchVocabularies];
 
    // ...
}

Now, implement the add action method. It brings up an alert view for the user to input a name of a new Vocabulary:

- (void)add
{
    UIAlertView * inputAlert = [[UIAlertView alloc] initWithTitle:@"New Vocabulary"
        message:@"Enter a name for the new vocabulary" delegate:self
        cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
    inputAlert.alertViewStyle = UIAlertViewStylePlainTextInput;
    [inputAlert show];
}

Finally, implement the alertView:clickedButtonAtIndex: delegate method to create the new Vocabulary if the user taps the OK button:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 1)
    {
        NSEntityDescription *vocabularyEntityDescription =
            [NSEntityDescription entityForName:@"Vocabulary"
                inManagedObjectContext:self.managedObjectContext];
        Vocabulary *newVocabulary = (Vocabulary *)[[NSManagedObject alloc]
            initWithEntity:vocabularyEntityDescription
            insertIntoManagedObjectContext:self.managedObjectContext];
        newVocabulary.name = [alertView textFieldAtIndex:0].text;
        NSError *error;
        if (![self.managedObjectContext save:&error])
        {
            NSLog(@"Error saving context: %@", error);
        }
        [self fetchVocabularies];
        [self.tableView reloadData];
    }
}

If you build and run the app again, you now can add new Vocabularies, as shown in Figure 12-16.

9781430245995_Fig12-16.jpg

Figure 12-16.  Adding a new Vocabulary

As a final feature of the Vocabularies view controller, you implement the possibility to delete items. Do that by implementing the tableView:commitEditingStyle:forRowAtIndexPath: delegate method, like so:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        NSManagedObject *deleted =
            [self.fetchedResultsController objectAtIndexPath:indexPath];
        [self.managedObjectContext deleteObject:deleted];
        NSError *error;
        BOOL success = [self.managedObjectContext save:&error];
        if (!success)
        {
            NSLog(@"Error saving context: %@", error);
        }
        [self fetchVocabularies];
        [self.tableView deleteRowsAtIndexPaths:@[indexPath]
            withRowAnimation:UITableViewRowAnimationRight];
    }
}

To test this feature, run the app again and swipe an item in the list. A red button then appears that allows you to delete the item in question. Figure 12-17 shows an example of this.

9781430245995_Fig12-17.jpg

Figure 12-17.  Deleting a Vocabulary

With the Vocabularies view all setup, it’s time to create the view that handles the words.

Implementing the Words View Controller

When the user selects a cell in the Vocabularies table view, another table view is presented displaying the words of that Vocabulary.

Create a new UITableViewController subclass named WordsViewController, again without checking the “With XIB” option.

You initialize the Words view controller with a Vocabulary, so you need a property and a custom initializer for that. You also need to import the Vocabulary and Word classes. Go to WordsViewController.h and add the following declarations:

//
//  WordsViewController.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "Vocabulary.h"
#import "Word.h"
 
@interface WordsViewController : UITableViewController
 
@property (strong, nonatomic)Vocabulary *vocabulary;
 
- (id)initWithVocabulary:(Vocabulary *)vocabulary;
 
@end

Now, switch to the WordsViewController.m. The initializer method simply assigns the vocabulary property and is pretty straightforward. Here are its implementations:

- (id)initWithVocabulary:(Vocabulary *)vocabulary
{
    self = [super initWithStyle:UITableViewStylePlain];
    if (self)
    {
        self.vocabulary = vocabulary;
    }
    return self;
}

The viewDidLoad method is even simpler (at this point), only setting the view controller’s title:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    self.title = self.vocabulary.name;
}

Here are the data source delegate methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.vocabulary.words.count;
}

Notice how you use the corresponding property of the Vocabulary entity’s words relationship to get the number of words. This is where the power of Core Data starts to show; you can handle the data as normal objects and ignore the fact that its actually stored in a database.

Next, you’ll design the table view cells to display both the word and its translation (as a subtitle.) You do that by adding the following implementation of the tableView:cellForRowAtIndexPath: delegate method:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"WordCell";
    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
            reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }
    Word *word = [self.vocabulary.words.allObjects objectAtIndex:indexPath.row];
    cell.textLabel.text = word.word;
    cell.detailTextLabel.text = word.translation;
    return cell;
}

To connect the two view controllers with each other, go back to VocabulariesViewController.h and import the Words view controller header file:

//
//  VocabulariesViewController.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "Vocabulary.h"
#import "WordsViewController.h"
 
@interface VocabulariesViewController : UITableViewController<UIAlertViewDelegate>
 
@property (strong, nonatomic)NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
 
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context;
 
@end

And finally, in the VocabulariesViewController.m file, add the following delegate method:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    Vocabulary *vocabulary = (Vocabulary *)[self.fetchedResultsController
        objectAtIndexPath:indexPath];
 
    WordsViewController *detailViewController =
        [[WordsViewController alloc] initWithVocabulary:vocabulary];
    [self.navigationController pushViewController:detailViewController animated:YES];
}

Now is a good time to build and run to be sure everything is okay so far. You can now select a Vocabulary and see its word list view, although empty at this point, as in Figure 12-18.

9781430245995_Fig12-18.jpg

Figure 12-18.  A Vocabulary with no words in it

The next step is to implement a way for the user to add Words to the Vocabulary, again in the form of an Add button on the Navigation Bar. But before you add that button, create the view controller that handles the editing of the new Word object.

Adding a Word Edit View

Create a new subclass of UIViewController (not UITableViewController as before) with the name EditWordViewController. You’ll build a user interface for it, so make sure the With XIB for user interface option is checked this time.

Open the EditWordViewController.xib file and build a user interface like the one in Figure 12-19.

9781430245995_Fig12-19.jpg

Figure 12-19.  A simple user interface for editing Words

As usual, create outlets for the text fields. Name them wordTextField and translationTextField, respectively.

With the user interface in place, you can move on to define the programming interface of this view controller. You use Objective-C blocks to simplify the code on the calling side, which uses the following class method to present the edit view controller:

+ (void)editWord:(Word *)word
    inNavigationController:(UINavigationController *)navigationController
    completion:(EditWordViewControllerCompletionHandler)completionHandler;

The EditWordViewControllerCompletionHandler is a block type with two arguments, sender and canceled:

typedef void (^EditWordViewControllerCompletionHandler)
    (EditWordViewController *sender, BOOL canceled);

To implement this API you need a couple of instance variables and a custom initializer method, as well. In all, add the following code to the EditWordViewController.h file:

//
//  EditWordViewController.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "Word.h"
 
@class EditWordViewController;
 
typedef void (^EditWordViewControllerCompletionHandler)(EditWordViewController *sender, BOOL canceled);
 
@interface EditWordViewController : UIViewController
{
@private
    EditWordViewControllerCompletionHandler _completionHandler;
    Word *_word;
}
 
@property (weak, nonatomic) IBOutlet UITextField *wordTextField;
@property (weak, nonatomic) IBOutlet UITextField *translationTextField;
 
- (id)initWithWord:(Word *)word
    completion:(EditWordViewControllerCompletionHandler)completionHandler;
 
+ (void)editWord:(Word *)word
    inNavigationController:(UINavigationController *)navigationController
    completion:(EditWordViewControllerCompletionHandler)completionHandler;
 
@end

Now, go to EditWordViewController.m and add the following implementation of the class method. It simply instantiates the edit view controller and pushes it onto the provided Navigation controller, like so:

+ (void)editWord:(Word *)word
    inNavigationController:(UINavigationController *)navigationController
    completion:(EditWordViewControllerCompletionHandler)completionHandler
{
    EditWordViewController *editViewController =
        [[EditWordViewController alloc] initWithWord:word completion:completionHandler];
    [navigationController pushViewController:editViewController animated:YES];
}

The initializer method stores away the Word and the Completion handler in the respective instance variable. Here, the implementation:

- (id)initWithWord:(Word *)word completion:(EditWordViewControllerCompletionHandler)completionHandler
{
    self = [super initWithNibName:nil bundle:nil];
    if (self)
    {
        _completionHandler = completionHandler;
        _word = word;
    }
    return self;
}

When the edit view controller loads, it updates the two text fields with data from the provided Word object. It also adds two buttons, Done and Cancel, to the Navigation Bar. To achieve that, add the following code to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
     self.title = @"Edit Word";
 
    self.wordTextField.text = _word.word;
    self.translationTextField.text = _word.translation;
    self.navigationItem.rightBarButtonItem =
        [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
            target:self action:@selector(done)];
    self.navigationItem.leftBarButtonItem =
        [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
            target:self action:@selector(cancel)];
}

As you can see from the code, two action methods have been hooked up to the buttons. Now implement those.

The first, done, updates the Word object with the data from the two text fields and then notifies the caller by invoking the completion handler block, sending NO for the cancel argument, like so:

- (void)done
{
    _word.word = self.wordTextField.text;
    _word.translation = self.translationTextField.text;
    _completionHandler(self, NO);
}

The cancel action method is even simpler. It’ll only notify the caller if the user has canceled the edit:

- (void)cancel
{
    _completionHandler(self, YES);
}

You’re now done with the edit view controller, so implement the code to display it. First, import the edit view controller in the WordsViewController.h file:

//
//  WordsViewController.h
//  My Vocabularies
//
 
#import <UIKit/UIKit.h>
#import "Vocabulary.h"
#import "Word.h"
#import "EditWordViewController.h"
 
@interface WordsViewController : UITableViewController
 
@property (strong, nonatomic)Vocabulary *vocabulary;
 
- (id)initWithVocabulary:(Vocabulary *)vocabulary;
 
@end

Then you add an Add button to the Navigation Bar of the Words view controller. Switch to WordsViewController.m and add the following lines to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBarButtonItem *addButton =
        [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
            target:self action:@selector(add)];
    self.navigationItem.rightBarButtonItem = addButton;
 
    self.title = self.vocabulary.name;
}

Next, start implementing the action method of this button. It creates a new Word object, which it provides as an argument to the edit view controller:

- (void)add
{
    NSEntityDescription *wordEntityDescription =
        [NSEntityDescription entityForName:@"Word"
            inManagedObjectContext:self.vocabulary.managedObjectContext];
    Word *newWord = (Word *)[[NSManagedObject alloc]
        initWithEntity:wordEntityDescription
        insertIntoManagedObjectContext:self.vocabulary.managedObjectContext];
 
    [EditWordViewController editWord:newWord
     inNavigationController:self.navigationController completion:
     ^(EditWordViewController *sender, BOOL canceled)
     {
         // TODO: Handle edit finished
     }];
}

When the edit view controller finishes, you either delete the new Word object if the user canceled, or add it to the vocabulary and save it to the database. Either way, the edit view controller should be popped from the Navigation controller. The complete implementation of the add action method should be as follows:

- (void)add
{
    NSEntityDescription *wordEntityDescription =
        [NSEntityDescription entityForName:@"Word"
            inManagedObjectContext:self.vocabulary.managedObjectContext];
    Word *newWord = (Word *)[[NSManagedObject alloc]
        initWithEntity:wordEntityDescription
        insertIntoManagedObjectContext:self.vocabulary.managedObjectContext];
    [EditWordViewController editWord:newWord
     inNavigationController:self.navigationController completion:
     ^(EditWordViewController *sender, BOOL canceled)
     {
         if (canceled)
         {
             [self.vocabulary.managedObjectContext deleteObject:newWord];
         }
         else
         {
             [self.vocabulary addWordsObject:newWord];
             NSError *error;
             if (![self.vocabulary.managedObjectContext save:&error])
             {
                 NSLog(@"Error saving context: %@", error);
             }
             [self.tableView reloadData];
         }
         [self.navigationController popViewControllerAnimated:YES];
     }];
}

If you build and run now, you can add words to your vocabularies using the Add button in the respective Words view. Figure 12-20 shows an example of this.

9781430245995_Fig12-20.jpg

Figure 12-20.  Adding Words to a Vocabulary

The user should of course be able to edit an existing word. You’ll implement it so that when a user selects a cell, the edit view for that Word is displayed. To do that, add the following implementation in WordsViewController.m to the tableView:didSelectRowAtIndexPath: delegate method:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    Word *word = [self.vocabulary.words.allObjects objectAtIndex:indexPath.row];
    [EditWordViewController editWord:word
     inNavigationController:self.navigationController completion:
     ^(EditWordViewController *sender, BOOL canceled)
     {
         NSError *error;
         if (![self.vocabulary.managedObjectContext save:&error])
         {
             NSLog(@"Error saving context: %@", error);
         }
         [self.tableView reloadData];
         [self.navigationController popViewControllerAnimated:YES];
     }];
}

To allow the user to delete Words, add the tableView:commitEditingStyle:forRowAtIndexPath: delegate method with the following implementation:

-(void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        Word *deleted = [self.vocabulary.words.allObjects objectAtIndex:indexPath.row];
        [self.vocabulary.managedObjectContext deleteObject:deleted];
        NSError *error;
        BOOL success = [self.vocabulary.managedObjectContext save:&error];
        if (!success)
        {
            NSLog(@"Error saving context: %@", error);
        }
        [self.tableView deleteRowsAtIndexPaths:@[indexPath]
            withRowAnimation:UITableViewRowAnimationRight];
    }
}

If you build and run now you can delete words by sweeping your finger (or mouse pointer if you run in the iOS Simulator), as shown in Figure 12-21.

9781430245995_Fig12-21.jpg

Figure 12-21.  Deleting a word in a Spanish vocabulary list

You’re almost finished with this simple word list app, but there is one small issue that we’d like you to fix before we close this recipe. You’ve probably noticed that if you add Words to a Vocabulary and return to the main view, the item count for the Vocabulary doesn’t update. The easiest way to fix this is to reload the data whenever the view appears. To do that, add the following method to the VocabulariesViewController.m file:

- (void)viewWillAppear:(BOOL)animated
{
    [self fetchVocabularies];
    [self.tableView reloadData];
}

Now, if you try again you’ll see that the number within the parentheses updates to reflect the new number of items in the Vocabulary (see Figure 12-22).

9781430245995_Fig12-22.jpg

Figure 12-22.  A word list app with two Vocabularies containing five and three words, respectively

In this recipe, we have covered the basics of Core Data, one of the most integral parts of iOS development. You have seen a glimpse of its power and simplicity of use when it comes to data modeling, persistence, and access. However, we have by no means detailed every facet in the Core Data framework, or even touched on many of the general subjects related to it. You can easily find entire books devoted to the subject of Core Data, and you probably should to get a more complete view of exactly how much ability you have in controlling how your data is stored. The overview here has demonstrated a basic use of the framework and explained the key concepts needed to get started working with Core Data, so that you can implement simple persistence in your applications without worrying about the more esoteric complexities. If you want to know more about this great framework, we recommend you start with Apple’s documentation on this topic.1

Persisting Data on iCloud

iCloud is Apple’s data storage service for iOS devices. If set up, iOS uses the service for things such as backups and synchronizing images between the user’s different devices. iCloud comes with an extensive API so that your apps too can take advantage of its features, including persisting data, and sharing state and files between all the user’s devices.

Basically, iCloud comes with three kinds of storage:

  • Key-value storage, which can be used to store preferences, settings, and other small-sized data.
  • Document storage, for file-based information such as images, text documents, files containing information about your app’s state, etc.
  • Core Data storage, which actually uses Document storage to persist and synchronize your app’s Core Data.

In Recipe 12-4, we show you how you can implement Key-value storage in your apps. Recipe 12-5 shows you how you can create and store custom documents in iCloud. If you’re interested in Core Data storage on iCloud, we recommend Apple’s documentation.2

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

Recipe 12-4: Storing Key-Value Data in iCloud

In this recipe, you set up an app for storing key-value data in iCloud. You use the key-value store to persist a simple user preference governing the font size of a displayed text. You start with the basic functionality of the app and then implement the persisting to iCloud.

Create a new single-view app project with the name “Testing iCloud.” Select the ViewController.xib file and start building a user interface resembling the one in Figure 12-23.

9781430245995_Fig12-23.jpg

Figure 12-23.  A simple user interface with a Text View and a Segmented Control

Create outlets for the controls and use the names fontSizeSegmentedControl and documentTextView, respectively. Also create an action named updateTextSize for the Segmented control.

As you’ve probably guessed, the user can change the size of the text in the Text View using the Segmented Control. To implement that, go to ViewController.m and add the following code to the updateTextSize action method:

- (IBAction)updateTextSize:(id)sender
{
    CGFloat newFontSize;
    switch (self.fontSizeSegmentedControl.selectedSegmentIndex)
    {
        case 1:
            newFontSize = 19;
            break;
        case 2:
            newFontSize = 24;
            break;
        default:
            newFontSize = 14;
            break;
    }
    self.documentTextView.font = [UIFont systemFontOfSize:newFontSize];
}

That’s it! You can now run the app and change the text size from the Segment Control, as shown in Figure 12-24.

9781430245995_Fig12-24.jpg

Figure 12-24.  Changing the text size with a Segmented Control

Notice that if you change the text size to, say, Large and kill the app, the preference is not persisted and will be back to Small when you run the app again. You will implement persistence next, but instead of using NSUserDefaults you’ll use iCloud’s key-value store. This advantage of using iCloud over local storage, is that the preference can be persisted, not only between executions on your device, but also shared by all your devices running this app. Additionally, if you remove and reinstall the app for some reason, the preferences will not be erased.

Let’s go ahead and implement this feature, but first you need to do some configuring tasks to set up your app with iCloud.

Setting Up iCloud For an App

First, you must configure “entitlements” for the project to allow for iCloud and its Key-Value store. Navigate to the project’s Target settings and scroll down to the section called “Entitlements.” Click the check boxes labeled “Entitlements,” “Enable iCloud,” and “Key-Value Store,” as shown in Figure 12-25. Xcode then automatically generates an entitlements file with the correct settings.

9781430245995_Fig12-25.jpg

Figure 12-25.  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 prompts you to enter a description, as well as a bundle identifier. Set the description to “Testing iCloud.” 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, as shown in Figure 12-26. It will most likely have a format along the lines of “com.domainName.Testing-iCloud.”

9781430245995_Fig12-26.jpg

Figure 12-26.  Finding an app’s bundle identifier

In Figure 12-26, my identifier is “com.hans-eric.Testing-iCloud”, so I enter this text in my browser. Figure 12-27 shows my Description and the bundle id configurations in Apple’s iOS provisioning portal.

9781430245995_Fig12-27.jpg

Figure 12-27.  Configuring an App ID in iOS provisioning portal

On creating this new App ID, you are 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 12-28. If a dialog appears warning you of having to manually regenerate profiles, simply click OK.

9781430245995_Fig12-28.jpg

Figure 12-28.  Enabling iCloud for your certificate

Click Done to finish configuring your App ID.

Next, click the Provisioning link in the table of contents on the left-hand side of the page. Then click the New Profile button to begin creating a new provisioning profile.

Name this new profile “Testing iCloud Profile.” Select your certificate that you should already have as an iOS developer. Set the App ID field to your recently made “Testing iCloud” App ID, and be sure to check the boxes next to whichever devices on which you want to test this application. Figure 12-29 shows my configuration screen, which yours should resemble with your own information.

9781430245995_Fig12-29.jpg

Figure 12-29.  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.

After 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 lists all 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 12-30.

9781430245995_Fig12-30.jpg

Figure 12-30.  Copying your new profile by dragging it into the Provisioning Profiles section of your device

At this point, your device is fully configured to run the project you have created. Because both your application and your device are configured to work with iCloud, you can continue to build your actual application.

Persisting Data in iCloud Key-Value Store

You’ll now move on to implement the storing of the text size preference in the iCloud Key-Value store. First, you’ll need a property to store a reference to the Key-Value store. Go to ViewController.h and add the following declaration:

//
//  ViewController.h
//  Testing iCloud
//
 
#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UISegmentedControl *fontSizeSegmentedControl;
@property (weak, nonatomic) IBOutlet UITextView *documentTextView;
@property (strong, nonatomic) NSUbiquitousKeyValueStore *iCloudKeyValueStore;
 
- (IBAction)updateTextSize:(id)sender;
 
@end

Now, switch to ViewController.m and add the following code to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.iCloudKeyValueStore = [NSUbiquitousKeyValueStore defaultStore];

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

    [self.iCloudKeyValueStore synchronize];
    [self updateUserInterfaceWithPreferences];
}

What the above code does is

  1. Get a reference to the Key-Value store
  2. Sign up for notifications when the data in the Key-Value store is changed by an external source (NSUbiquitousKeyValueStoreDidChangeExternallyNotification)
  3. Make sure the Key-Value store cache is up-to-date by calling synchronize
  4. Update the user interface with the values from the Key-Value store

Note  Here, you’re setting up the iCloud access directly in the main view controller, which is fine for the purpose of this recipe. However, in an app with several view controllers that access the Key-Value store, you should set it up in the App delegate instead, and distribute the reference to the regarded parties.

Next, implement the notification handler for when the iCloud data is changed by an external source. For the sake of this recipe, you’ll simply update the user interface with the new values:

- (void)handleStoreChange:(NSNotification *)notification
{
    [self updateUserInterfaceWithPreferences];
}

The updateUserInterfaceWithPreference helper method extracts the text size value from the Key-Value store and sets the selected index of the Segment Control:

- (void)updateUserInterfaceWithPreferences
{
    NSInteger selectedSize = [self.iCloudKeyValueStore doubleForKey:@"TextSize"];
    self.sizeSegmentedControl.selectedSegmentIndex = selectedSize;
    [self updateTextSize:self];
}

Finally, when the user changes the text size using the Segment Control, you should write the new value to the Key-Value store for persistency in iCloud. Add the following code to the updateTextSize: action method:

- (IBAction)updateTextSize:(id)sender
{
    CGFloat newFontSize;
    switch (self.fontSizeSegmentedControl.selectedSegmentIndex)
    {
        case 1:
            newFontSize = 19;
            break;
        case 2:
            newFontSize = 24;
            break;
        default:
            newFontSize = 14;
            break;
    }
    self.documentTextView.font = [UIFont systemFontOfSize:newFontSize];
    // Update Preferences
    NSInteger selectedSize = self.fontSizeSegmentedControl.selectedSegmentIndex;
    [self.iCloudKeyValueStore setDouble:selectedSize forKey:@"TextSize"];
}

Note  You’re using the setDouble:forKey: method to store an Integer value here, but you can store any kind of Key-Value compliant data, e.g. NSStrings, BOOLs, NSData objects, or even NSArrays and NSDictionary objects.

That’s all you need to do to store the preference value in iCloud. But before you can 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. For this application to properly store data, your iCloud account must be properly configured and verified. This requires you to have verified your email address and registered it as your Apple ID. The item marked “Documents & Data” should also be set to “ON”, as in Figure 12-31. You can, of course, easily configure this once your account is verified.

9781430245995_Fig12-31.jpg

Figure 12-31.  Documents and Data must be enabled to store information in iCloud

With iCloud setup on your device, you can build and run the app to test its new persistency feature. You should now be able to do the following:

  • Set the text size preference to Medium or Large, kill the app and restart it; it should now automatically set the preference to the value you chose.
  • Set the text size preference to Medium or Large, uninstall the app from the device and then reinstall it; in a second or two, it should automatically set the preference to the value it had before uninstalling.
  • Run the app on two different devices, change the preference on one device and see it automatically reflected in the other (it may take some time before the change takes place).

This is all well and good; however, there is a problem with this implementation. It will not work if iCloud is turned off or unavailable. You can easily test this by running the app in the iOS simulator (which doesn’t have support for iCloud). You’ll see there, that the persistency doesn’t work and the app is reset to the Small text size on every launch.

For these reasons, it’s recommended that you in addition to the iCloud Key-Value store, save the values in a local NSUserDefaults cache as well. This makes your app more robust and resilient to problems stemming from iCloud access problems. Fortunately, this is an easy fix as the next section shows.

Caching iCloud Data Locally Using NSUserDefaults

Start by adding an NSUserDefaults property in the ViewController.h file:

//
//  ViewController.h
//  Testing iCloud
//
 
#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UISegmentedControl *fontSizeSegmentedControl;
@property (weak, nonatomic) IBOutlet UITextView *documentTextView;
@property (strong, nonatomic) NSUbiquitousKeyValueStore *iCloudKeyValueStore;
@property (strong, nonatomic) NSUserDefaults *userDefaults;
 
- (IBAction)updateTextSize:(id)sender;
 
@end

There’s only a few changes needed for setting up the local cache. First, in viewDidLoad you’ll initialize the property:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.iCloudKeyValueStore = [NSUbiquitousKeyValueStore defaultStore];

    self.userDefaults = [NSUserDefaults standardUserDefaults];
    // ...
}

Next, when the preference value is written to iCloud, you’ll write the same value to the NSUserDefaults as well. To do this, add the following line to the updateTextSize: method:

- (IBAction)updateTextSize:(id)sender
{
    // ...
 
    // Update Preferences
    NSInteger selectedSize = self.sizeSegmentedControl.selectedSegmentIndex;
    [self.userDefaults setDouble:selectedSize forKey:@"TextSize"];
    [self.userDefaults synchronize];
    [self.iCloudKeyValueStore setDouble:selectedSize forKey:@"TextSize"];
}

Finally, when updating user interface with the preferences, instead of just blindly taking the value from the iCloud Key-Value store, you’ll first check and see if it exists. If it doesn’t, you’ll use the value from the local cache instead. Here is the new implementation of the updateUserInterfaceWithPreferences method:

- (void)updateUserInterfaceWithPreferences
{
    NSInteger selectedSize;
    if ([self.iCloudKeyValueStore objectForKey:@"TextSize"] != nil)
    {
        // iCloud value exists
        selectedSize = [self.iCloudKeyValueStore doubleForKey:@"TextSize"];
        // Make sure local cache is synced
        [self.userDefaults setDouble:selectedSize forKey:@"TextSize"];
        [self.userDefaults synchronize];
    }
    else
    {
        // iCloud unavailable, use value from local cache
        selectedSize = [self.userDefaults doubleForKey:@"TextSize"];
    }
 
    self. fontSizeSegmentedControl.selectedSegmentIndex = selectedSize;
    [self updateTextSize:self];
}

Now the app should work and persist its size text preference, both with iCloud and without it.

As you can see, working with iCloud Key-Value store is extremely simple. There’s but one problem: you cannot store big chunks of data. There is a limit of 1MB per application and you can use no more than 1,024 keys to store the data. This makes it a poor candidate for application data model storage. For that, you can use the Documents store, which is what the next recipe will be about.

Recipe 12-5: Storing UIDocuments in iCloud

Besides the Key-Value store, an iCloud account may consist of one or more so called ubiquity containers. A ubiquity container is like a file folder on your device that is automatically synced with a corresponding file folder in iCloud. Using the UIDocument API, you can create custom documents and store them in such a ubiquity container.

In this recipe, you’ll build on the project from Recipe 12-4 and allow the user to store the text as a document in iCloud. You’ll start by adding a ubiquity container to the app’s “Entitlements.” Navigate to the Target settings and its “Entitlements” section. Click on the + button beneath the Ubiquity Containers box. This adds the default container, with the same name as the project’s Bundle identifier, as shown in Figure 12-32.

9781430245995_Fig12-32.jpg

Figure 12-32.  Adding a ubiquity container to the project

Next, you’ll make a small change to the user interface. Open the ViewController.xib file and add a button as shown in Figure 12-33. Notice that we’ve decreased the height of the text view so that the Save button won’t be concealed by the keyboard when the user enters text.

9781430245995_Fig12-33.jpg

Figure 12-33.  The user interface with an added Save button for storing the text document in iCloud

Also, create an action with the name saveDocument for the Save button.

Next, you’ll create a UIDocument subclass to handle the saving and loading of the text. Name the new class “MyDocument.”

The document has two properties, one for the text and one for a delegate used to notify the view controller that the remote text document has changed. Open MyDocument.h and add the following code:

//
//  MyDocument.h
//  Testing iCloud
//
 
#import <UIKit/UIKit.h>
 
@class MyDocument;
 
@protocol MyDocumentDelegate <NSObject>
- (void)documentDidChange:(MyDocument*)document;
@end
 
@interface MyDocument : UIDocument
 
@property (strong, nonatomic) NSString *text;
@property (weak, nonatomic) id<MyDocumentDelegate> delegate;
 
@end

The UIDocument class requires you to implement two methods. The first is contentsForType:error:, which is used to encode the data into its storing format. In this case, you save the string with an UTF8 encoding:

- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
    if (!self.text)
        self.text = @"";
    return [self.text dataUsingEncoding:NSUTF8StringEncoding];
}

The other method, loadFromContents:ofType:error:, does the reverse, building an NSString out of raw data and setting it to your property. In this implementation, this method also invokes the delegate to notify the view controller of the content change:

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

Now that your data model is configured (yes, it is that simple!), you can move on to implement the persisting of the document. Go to the ViewController.h file and add two property declarations, one for referencing the document and one for the document’s URL. Also, add the MyDocumentDelegate protocol to prepare the view controller for being the document’s delegate. The ViewController.h file should now look like this:

//
//  ViewController.h
//  Testing iCloud
//
 
#import <UIKit/UIKit.h>
#import "MyDocument.h"
 
@interface ViewController : UIViewController <MyDocumentDelegate>
 
@property (weak, nonatomic) IBOutlet UISegmentedControl *fontSizeSegmentedControl;
@property (weak, nonatomic) IBOutlet UITextView *documentTextView;
@property (strong, nonatomic) NSUbiquitousKeyValueStore *iCloudKeyValueStore;
@property (strong, nonatomic) NSUserDefaults *userDefaults;
@property (strong, nonatomic) MyDocument *document;
@property (strong, nonatomic) NSURL *documentURL;
 
- (IBAction)updateTextSize:(id)sender;
- (IBAction)saveDocument:(id)sender;
 
@end

Now, go to ViewController.m. In the viewDidLoad, add the following line to initiate an update of the Text View with the persisted text, if existent:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.iCloudKeyValueStore = [NSUbiquitousKeyValueStore defaultStore];
    self.userDefaults = [NSUserDefaults standardUserDefaults];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(handleStoreChange:)
        name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
        object:self.iCloudKeyValueStore];
    [self.iCloudKeyValueStore synchronize];
    [self updateUserInterfaceWithPreferences];
 
    [self updateDocument];
}

The updateDocument first checks to see whether iCloud is available:

- (void)updateDocument
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    id iCloudToken = [fileManager ubiquityIdentityToken];
    if (iCloudToken)
    {
        // iCloud available
 
        // Register to notifications for changes in availability
        [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(handleICloudDidChangeIdentity:)
            name:NSUbiquityIdentityDidChangeNotification object:nil];
 
        //TODO: Open existing document or create new
    }
    else
    {
        // No iCloud access
        self.documentURL = nil;
        self.document = nil;
        self.documentTextView.text = @"<NO iCloud Access>";
    }
}

If iCloud is available, updateDocument will create an instance of MyDocument and either open it if it exists in the ubiquity container, or save it to upload it. To avoid freezing the user interface during this time, it’ll perform these actions on a different thread:

- (void)updateDocument
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    id iCloudToken = [fileManager ubiquityIdentityToken];
    if (iCloudToken)
    {
        // iCloud available
 
        // Register to notifications for changes in availability
        [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(handleICloudDidChangeIdentity:)
            name:NSUbiquityIdentityDidChangeNotification object:nil];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
        ^{
             NSURL *documentContainer = [[fileManager URLForUbiquityContainerIdentifier:nil]
                 URLByAppendingPathComponent:@"Documents"];
             if (documentContainer != nil)
             {
                 self.documentURL =
                     [documentContainer URLByAppendingPathComponent:@"mydocument.txt"];
                 self.document =
                     [[MyDocument alloc] initWithFileURL:self.documentURL];
                     self.document.delegate = self;
                     // If the file exists, open it; otherwise, create it.
                     if ([fileManager fileExistsAtPath:self.documentURL.path])
                         [self.document openWithCompletionHandler:nil];
                     else
                         [self.document saveToURL:self.documentURL
                             forSaveOperation:UIDocumentSaveForCreating
                             completionHandler:nil];
             }
        });
    }
    else
    {
        // No iCloud access
        self.documentURL = nil;
        self.document = nil;
        self.documentTextView.text = @"<NO iCloud Access>";
    }
}

To handle if the user logs out of iCloud, or changes to a different account, add the following implementation of the handleICloudDidChangeIdentity: notification method. It simply calls the updateDocument helper method:

- (void)handleICloudDidChangeIdentity: (NSNotification *)notification
{
    NSLog(@"ID changed");
    [self updateDocument];
}

When the document content changes, the app should update the Text View. This is done in the documentDidChange: delegate method. Because it may be called on an arbitrary thread, you need to make sure the updating is run on the main thread, like so:

- (void)documentDidChange:(MyDocument *)document
{
    dispatch_async(dispatch_get_main_queue(),
    ^{
        self.documentTextView.text = document.text;
    });
}

Finally, when the user taps the Save button, the document shall be updated with the new text and saved to iCloud. To do that, add the following implementation of the saveDocument: action method:

- (IBAction)saveDocument:(id)sender
{
    if (self.document)
    {
        self.document.text = self.documentTextView.text;
        [self.document saveToURL:self.documentURL
            forSaveOperation:UIDocumentSaveForOverwriting completionHandler:
         ^(BOOL success)
         {
             if (success)
             {
                 NSLog(@"Written to iCloud");
             }
             else
             {
                 NSLog(@"Error writing to iCloud");
             }
         }];
    }
}

That’s it! Assuming your device is correctly configured, your simple application can store the document 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 12-34.

9781430245995_Fig12-34.jpg

Figure 12-34.  Your application with text saved and loaded from iCloud

Summary

Data persistence is one of the most important considerations in developing an application. Developers must consider the type of data they want 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 on which approach to use to store data, whether it is the simple NSUserDefaults method, the file management system, or the intricate Core Data framework. On top of this, the relatively new addition of 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 significantly more relevant. By firmly understanding the most up-to-date concepts of data persistence in iOS, you can always keep your users updated with the fastest, most efficient, and most powerful methods of storing data possible.

1 https://developer.apple.com/library/mac/#documentation/cocoa/conceptual/coredata/cdprogrammingguide.html

2 https://developer.apple.com/library/ios/#documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesignForCoreDataIniCloud.html#//apple_ref/doc/uid/TP40012094-CH3-SW1

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

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