Chapter     15

Data Storage Recipes

When working in iOS, one of the most important concepts to understand is the implementation of persistence. This means having information saved and retrieved, or persisted, 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 values to the most complex data structures—to stay stored in your device for indefinite periods of time.

In this chapter, we cover many data persistence methods that will allow you to save data for offline use including user defaults, documents, databases, and even iCloud. We will show you the different advantages, disadvantages, general uses, and complexities of each method so you can develop a full understanding of the best method of storage for any given situation.

Recipe 15-1. Persisting Data with NSUserDefaults

When developing applications, you often run into situations where you want to store simple values, such as 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 the NSUserDefaults class, built specifically for such simple situations.

The NSUserDefaults class uses simple APIs 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; for example, images should not be stored with NSUserDefaults.

In this recipe, you build a simple app that has a state that will persist using NSUserDefaults. Start by creating a new single view application and name it whatever you want. We’ve titled ours “Recipe 15-1 Persisting Data with NSUserDefaults.”

Set up the user interface of this app. Select the Main.storyboard file to start editing the main view. Add two text fields, a switch and an activity indicator, and set them up so the user interface resembles Figure 15-1.

9781430259596_Fig15-01.jpg

Figure 15-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. The ViewController.h file should now resemble Listing 15-1.

Listing 15-1.  The Complete ViewController.h File

//
//  ViewController.h
//  Recipe 15-1 Persisting Data with NSUserDefaults
//

#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

Now start implementing the basic functionality of the controls, starting with the text fields. Open the ViewController.m file and add the code in Listing 15-2 to the viewDidLoad method.

Listing 15-2.  Setting the Text Field Delegates

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.firstNameTextField.delegate = self;
    self.lastNameTextField.delegate = self;
}

Next, add the delegate method shown in Listing 15-3 to the implementation file. It makes sure the keyboard gets removed if the user taps the Return button.

Listing 15-3.  Implementing the textFieldShouldReturn: Delegate Method

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

Now it’s time to implement the behavior of the switch. Listing 15-4 shows the implementation of the toggleActivity: action method.

Listing 15-4.  Implementing 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, you need to do two things 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 can either store the data whenever it’s changed or 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 terminated but put to sleep and can be reactivated and brought back to the same state without the need for persisting its data. However, in the case of low-memory conditions, an app can be terminated without warning. Because there is no way to know whether your app is being terminated, you should always be sure 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, which provides us with a lightweight custom delegation behavior, and register an observer of UIApplicationDidEnterBackgroundNotification. A good place to do this is when the view is loaded, so add the code in Listing 15-5 to viewDidLoad.

Listing 15-5.  Registering an NSNotificationCenter Observer

- (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, as shown in Listing 15-6.

Listing 15-6.  Implementation of the savePersistentData: Method

- (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 a method to perform the loading, as shown in Listing 15-7.

Listing 15-7.  Implementing the loadPersistentData: Method

- (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];
    }
}

Finally, call the loadPersistentData: method from the viewDidLoad method, as shown in Listing 15-8.

Listing 15-8.  Calling the loadPersistentData: Method from Within 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 persistency 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 terminate the app before relaunching it. To do that, you can either stop the app’s execution from Xcode or double-press the Home button and locate the “stubborn” app in the list of suspended apps; if you flick the app preview upward, it will shoot off the screen and close the app.

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

9781430259596_Fig15-02.jpg

Figure 15-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, 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 15-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 use the iOS 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 “Recipe 15-2 Persisting Data Using Files.”

Next, build a user interfacethat resembles Figure 15-3. You’ll need a label, a text field, a text view, and three buttons.

9781430259596_Fig15-03.jpg

Figure 15-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 file name into an absolute file path within the Documents directory of the device by adding the method shown in Listing 15-9 to the ViewController.m file.

Listing 15-9.  Implementing the currentContentFilePath Method

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

When the user taps the Save button, the app will need to save the content of the text view to the file path provided by the helper method you just created. Add the implementation shown in Listing 15-10 to the saveContent: action method.

Listing 15-10.  Implementing 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, the app will load the content from the file and update the text view. Listing 15-11 shows the implementation of the loadContent: action method.

Listing 15-11.  Implementing the loadContent: 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 using the action method shown in Listing 15-12.

Listing 15-12.  Implementation of the clearContent: Action Method

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

You now have a very rudimentary text file editor, so try it. Build and run the app. Enter some text in the text view, enter a file name in the Filename text input, and click the Save button. The app will create 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 it has 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 that 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 might or might 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. Do this by changing the implementation of the saveContent: method. Start by extracting the actual saving into a helper method called saveContentToFile, as shown in Listing 15-13.

Listing 15-13.  Implementation of the saveContentToFile: Method

- (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);
    }
}

Make changes, as shown in bold in Listing 15-14, to the saveContent: method.

Listing 15-14.  Updating the saveContent: Method to Alert the User Before Saving

- (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];
}

Add the UIAlertViewDelegate protocol to the ViewController.h file so the view controller can act as the alert view’s delegate and intercept when the user taps its buttons. Listing 15-15 shows this change.

Listing 15-15.  Declaring the UIAlertViewDelegate Protocol in the ViewController.h File

//
//  ViewController.h
//  Recipe 15-2 Persisting Data Using Files
//

#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 delegate method shown in Listing 15-16 for when the user taps one of the alert view’s buttons.

Listing 15-16.  Implementing the alertView:clickedButtonAtIndex: Delegate Method

- (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 15-4).

9781430259596_Fig15-04.jpg

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

In this demo app, you’ve worked only 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 does.

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 15-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 a 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 make 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 pregenerated methods. Aside from that, you will have no reason to deal with this class programmatically.
  • NSPersistentStoreCoordinator: This class, too, is one that you 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 four types of persistent stores.
  • NSSQLiteStoreType: A database store built on SQLite
  • NSBinaryStoreType: A binary store type
  • NSInMemoryStoreType: An in-memory store
  • NSXMLStoreType: A store type using XML

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

  • NSManagedObjectContext: This class, unlike the preceding two, is one you will be dealing with 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 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 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 set up Core Data for your app is to let Xcode generate the necessary code when you create the project. Create a new project called “Recipe 15-3 Using Core Data” using the Empty Application template, as shown in Figure 15-5.

9781430259596_Fig15-05.jpg

Figure 15-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 Use Core Data check box (see Figure 15-6).

9781430259596_Fig15-06.jpg

Figure 15-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 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 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 15-7 shows a diagram of this two-way relationship.

9781430259596_Fig15-07.jpg

Figure 15-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 15-1.

Table 15-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 are used for to-one relationships.

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

9781430259596_Fig15-08.jpg

Figure 15-8. The Data Model Editor with an empty data model

Now add the two entities of your data model. You can do this either by using 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 will need to click the entity, rename it “Vocabulary,” and then click 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 15-9.

9781430259596_Fig15-09.jpg

Figure 15-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 shown in Figure 15-10.

9781430259596_Fig15-10.jpg

Figure 15-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 set up for the Vocabulary entity should at this point be as in Figure 15-11.

9781430259596_Fig15-11.jpg

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

Now you will define this relationship as a to-many relationship. Do that by selecting one of the relationships and check the To-Many relationship option in the Data Model Inspector (which corresponds to the attributes inspector for view elements), as shown in Figure 15-12.

9781430259596_Fig15-12.jpg

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

While you most likely will not need to 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 setting 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 might use this, for example, in the case of a “folder” with multiple objects. When a folder is deleted, you should 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.

Change the Delete Rule to Cascade for this recipe so that if we delete a vocabulary, the words that pertain to it are deleted as well.

Now the time 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 15-13. This automatically sets up the inverse relationship for the words relationship as well (to vocabulary).

9781430259596_Fig15-13.jpg

Figure 15-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 belong only 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, create Objective-C classes that map to the respective entity. Make sure the Vocabulary entity is selected, go to the Editor menu and choose Create NSManagedObject Subclass. Then select both the Vocabulary entity and the Word entity, click the Next button, and then click the Create button. This adds new classes to the project named Vocabulary and Word, respectively.

This is all you need to do to create your data model. To get a graphic overview of the data model, change 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 their relationships, where a single arrow represents a to-one relationship and a double arrow represents a to-many relationship. The blocks might initially appear all stacked on top of each other, but if you drag them apart, your display should resemble Figure 15-14.

9781430259596_Fig15-14.jpg

Figure 15-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 will 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 a .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 changes to the VocabulariesViewController.h file, as shown in Listing 15-17.

Listing 15-17.  Implementing the alertView:clickedButtonAtIndex: Delegate Method

//
//  VocabulariesViewController.h
//  Recipe 15-3 Using Core Data
//

#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 about the code in Listing 15-16 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 might 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. You’ll want to replace the existing initWithStyle method, as shown with the initializer in Listing 15-18.

Listing 15-18.  Implementing the initWithManagedObjectContext: Initializer Method

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

Next, add the helper method in Listing 15-19, which fetches all vocabularies in the data model and stores them in the fetchedResultsController property.

Listing 15-19.  Implementing the fetchVocabularies Method

-(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 method in Listing 15-19 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 whether 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 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 loading the vocabularies, as shown in Listing 15-20.

Listing 15-20.  Modifying the viewDidLoad Method

- (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 this, add the code shown in bold in Listing 15-21 to the viewDidLoad method.

Listing 15-21.  Modifying the viewDidLoad Method to Preload with Vocabulary

- (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, implement the methods to specify the number of sections and rows shown in Listing 15-22.

Listing 15-22.  Implementing the numberOfSectionsInTableView: and tableView:numberOfRowsInSection: Delegate Methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

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

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

Listing 15-23 shows the method to configure the cells of the table view.

Listing 15-23.  Implementing the tableView:cellForRowAtIndexPath: Delegate Method

- (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 make it work, so go to the AppDelegate.h file and add the declarations shown in Listing 15-24.

Listing 15-24.  Adding Declarations to the appDelegate

//
//  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 code in Listing 15-24, the appDelegate is the place where Core Data has been set up for you. All you need to do is distribute the managed object context to the parts of your app that deal with the data.

In the application:didFinishLaunchingWithOptions: method in AppDelegate.m, add the code in Listing 15-25 to create and display the view controller in a navigation controller.

Listing 15-25.  Creating and Displaying Both the View and Navigation Controllers

- (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 go right, you should see a screen resembling Figure 15-15.

9781430259596_Fig15-15.jpg

Figure 15-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 code shown in Listing 15-26.

Listing 15-26.  Adding an “Add” Button to the Navigation Bar

- (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, as shown in Listing 15-27. It brings up an alert view for the user to input a name of a new vocabulary.

Listing 15-27.  Implementing the Add Action Method

- (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. Listing 15-28 shows this implementation.

Listing 15-28.  Implementing the alertView:clickedButtonAtIndex: Delegate Method

- (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 15-16.

9781430259596_Fig15-16.jpg

Figure 15-16. Adding a new vocabulary

As a final feature of the Vocabularies view controller, implement the ability to delete items. Do that by implementing the tableView:commitEditingStyle:forRowAtIndexPath: delegate method, as shown in Listing 15-29.

Listing 15-29.  Implementing the tableView:commitEditingStyle:forRowAtIndexPath: Delegate Method

-(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 should appear that allows you to delete the item in question. Figure 15-17 shows an example of this.

9781430259596_Fig15-17.jpg

Figure 15-17. Deleting a vocabulary

With the Vocabularies view all set up, 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 selecting the With XIB option.

You will 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 declarations shown in Listing 15-30.

Listing 15-30.  Updating the WordsViewController.h file for Words Implementation

//
//  WordsViewController.h
//  Recipe 15-3 Using Core Data
//

#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 file. The initializer method simply assigns the vocabulary property and is pretty straightforward. Listing 15-31 show the implementation.

Listing 15-31.  Implementing the initWithVocabulary: Initializer Method

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

The viewDidLoad method is more simple (at this point), setting only the view controller’s title, as shown in Listing 15-32.

Listing 15-32.  Setting the Title in the viewDidLoad Method

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = self.vocabulary.name;
}

Listing 15-33 shows the required data source delegate methods.

Listing 15-33.  Implementing Required Delegate Methods for the Number of Rows and Sections in the Table View

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

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

Notice in Listing 15-33 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 it’s actually stored in a database.

Next, you’ll design the table view cells to display both the word and its translation (as a subtitle). Do that by adding the implementation of the tableView:cellForRowAtIndexPath: delegate method shown in Listing 15-34.

Listing 15-34.  Implementing 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, as shown in Listing 15-35.

Listing 15-35.  Adding an Import Statement to the VocabulariesViewController.h File

//
//  VocabulariesViewController.h
//  Recipe 15-3 Using Core Data
//

#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

Finally, in the VocabulariesViewController.m file, add the delegate method shown in Listing 15-36.

Listing 15-36.  Implementing the tableView:didSelectRowAtIndexPath: 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 working properly. You can select a vocabulary and see its word list view, although empty at this point, as in Figure 15-18.

9781430259596_Fig15-18.jpg

Figure 15-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 selected this time.

Open the EditWordViewController.xib file and build a user interface like the one in Figure 15-19. Because we’ll be using a navigation controller, add the opaque navigation bar to the view by selecting it from the top bar drop-down in the attributes inspector with the full view selected.

9781430259596_Fig15-19.jpg

Figure 15-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 defining the programming interface of this view controller. You use Objective-C blocks to simplify the code on the calling side, which uses the class method shown in Listing 15-37 to present the edit view controller.

Listing 15-37.  Declaring the editWord: Class Method in the EditWordViewController.h File

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

The EditWordViewControllerCompletionHandler is a block type with two arguments, sender and canceled, as shown in Listing 15-38.

Listing 15-38.  Creating the Block Type Declaration

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

To implement this API, you need a couple of instance variables and a custom initializer method. In all, add the bold code in Listing 15-39 to the EditWordViewController.h file.

Listing 15-39.  The Complete EditWordViewController.h File

//
//  EditWordViewController.h
//  Recipe 15-3 Using Core Data
//

#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 implementation in Listing 15-40 of the class method. It simply instantiates the edit view controller and adds it to the navigation controller stack.

Listing 15-40.  Implementing the editWord: Class Method

+ (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. Listing 15-41 shows this implementation.

Listing 15-41.  Implementation of the initWithWord: Initializer Method

- (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 code in Listing 15-42 to the viewDidLoad method.

Listing 15-42.  Modifying the viewDidLoad Method to Set Text Fields and Add Buttons

- (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 you will need to implement them.

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, as shown in Listing 15-43.

Listing 15-43.  Implementing the Done Action Method

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

The cancel action method is even simpler. It will notify the caller only if the user has canceled the edit, as shown in Listing 15-44.

Listing 15-44.  Implementing the Cancel Action Method

- (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, as shown in Listing 15-45.

Listing 15-45.  Importing the EditWordViewController.h File into the WordsViewController.h File

//
//  WordsViewController.h
//  Recipe 15-3 Using Core Data
//

#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

Next, add an “Add” button to the navigation bar of the words view controller class. Switch to the WordsViewController.m file and modify the viewDidLoad method, as shown in bold in Listing 15-46.

Listing 15-46.  Creating an “Add” Button the WordsViewController Navigation Bar

- (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, as shown in Listing 15-47. It creates a new Word object, which it provides as an argument to the edit view controller.

Listing 15-47.  The Starting Implementation of the Add Action Method

- (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 look like Listing 15-48.

Listing 15-48.  The Complete Add Action Method Implementation

- (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 15-20 shows an example of this.

9781430259596_Fig15-20.jpg

Figure 15-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 implementation shown in Listing 15-49 of the WordsViewController.m file to the tableView:didSelectRowAtIndexPath: delegate method.

Listing 15-49.  Implementing the tableView:didSelectRowAtIndexPath: 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, as shown in Listing 15-50.

Listing 15-50.  Implementing the tableView:commitEditingstyle:forRowAtIndexPath: Delegate Method

-(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 15-21.

9781430259596_Fig15-21.jpg

Figure 15-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 method to the VocabulariesViewController.m file shown in Listing 15-51.

Listing 15-51.  Implementing the viewWillAppear Override to Reload the Table

- (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 15-22).

9781430259596_Fig15-22.jpg

Figure 15-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 consult those sources 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.

Persisting Data on iCloud

iCloud is Apple’s data storage service for iOS and Mac devices. If iCloud is set up, iOS uses the service for tasks 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. The other benefit is you can easily share information such as calendars between iOS and Mac devices. With the new Xcode 5 capabilities feature, which allows you to easily add capabilities such as iCloud and Game Center, it is now much less of a headache to set up iCloud services than with previous versions of Xcode.

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, and so on
  • Core Data storage, which actually uses document storage to persist and synchronize your app’s Core Data

In Recipe 15-4, we show you how you can implement key-value storage in your apps. Recipe 15-5 shows 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.

Note   The next recipe will require access to an iOS development program account as well as a physical iOS device. You can go to http://developer.apple.com to set up an account if you don’t already have one.

Recipe 15-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 persisting to iCloud.

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

9781430259596_Fig15-23.jpg

Figure 15-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 code in Listing 15-52 to the updateTextSize action method.

Listing 15-52.  Implementing 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 segmented control, as shown in Figure 15-24.

9781430259596_Fig15-24.jpg

Figure 15-24. Changing the text size with a segmented control

Notice that if you change the text size to, say, Large and terminate 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. The 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 or log in and out of iCloud 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

To set up iCloud, you must configure entitlements, which are special keys that allow you to use iCloud storage. Entitlements will be necessary to allow for iCloud and its key-value store. Navigate to the project’s target settings and scroll and select the Capabilities tab. Select the switch to turn on iCloud and check Use Key-Value Store, as shown in Figure 15-25. Xcode then automatically generates an entitlements file with the correct settings.

9781430259596_Fig15-25.jpg

Figure 15-25. Enabling entitlements to allow communication with iCloud

That is all that is needed to set up iCloud in iOS 7. Before iOS 7 and Xcode 5, setting up iCloud involved logging into the member center of the developer site and carrying out a few different steps. This was an annoying process to say the least. Now Xcode does everything for you behind the scenes.

Persisting Data in an 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 declaration shown in Listing 15-53.

Listing 15-53.  Adding an NSUbiquitousKeyValueStore to the ViewController.h File

//
//  ViewController.h
//  Recipe 15-4 Storing Key-Value Data in 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 code in Listing 15-54 to the viewDidLoad method.

Listing 15-54.  Setting Up iCloud in 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];
}

The code in Listing 15-54 does the following:

  1. Gets a reference to the key-value store
  2. Signs up for notifications when the data in the key-value store is changed by an external source (NSUbiquitousKeyValueStoreDidChangeExternallyNotification)
  3. Makes sure the key-value store cache is up-to-date by calling synchronize
  4. Updates 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 whichever devices request it.

Next, implement the notification handler for when the iCloud data is changed by an external source, such as another device. For the sake of this recipe, you’ll simply update the user interface with the new values shown in Listing 15-55.

Listing 15-55.  Implementing the handleStoreChange: Method

- (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 segmented control. This implementation is shown in Listing 15-56.

Listing 15-56.  Implementing the updateUserInterfaceWithPreferences Method

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

Finally, when the user changes the text size using the segmented control, you should write the new value to the key-value store for persistency in iCloud. Add the code to the updateTextSize: action method, as shown in Listing 15-57.

Listing 15-57.  Updating the updateTextSize: Method to Write to the Key-Value Store

- (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, such as 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 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 store data, your iCloud account must be properly configured and verified. This requires you to have verified your e-mail address and registered it as your Apple ID. The Documents & Data item should also be set to ON, as in Figure 15-26. You can, of course, easily configure this once your account is verified.

9781430259596_Fig15-26.jpg

Figure 15-26. Documents & Data must be enabled to store information in iCloud

With iCloud set up 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, terminate 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 might 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 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, in addition to the iCloud key-value store, you 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, as shown in Listing 15-58.

Listing 15-58.  Adding an NSUserDefaults Property to the ViewController.h File

//
//  ViewController.h
//  Recipe 15-4 Storing Key-Value Data in 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 are only a few changes needed for setting up the local cache. First, in viewDidLoad you’ll initialize the property, as shown in Listing 15-59.

Listing 15-59.  Initializing the userDefaults Property from the viewDidLoad Method

- (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. To do this, add the bold lines in Listing 15-60 to the updateTextSize: method.

Listing 15-60.  Updating the updateTextSize Method Add Value to NSUserDefaults

- (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 the user interface with the preferences, instead of just blindly taking the value from the iCloud key-value store, you’ll first check and see whether it exists. If it doesn’t, you’ll use the value from the local cache instead. Listing 15-61 shows the new implementation of the updateUserInterfaceWithPreferences method.

Listing 15-61.  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 1 MB 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 document store, which is what the next recipe is about.

Recipe 15-5. Storing UIDocuments in iCloud

Besides the key-value store, an iCloud account might consist of one or more 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 15-4 and allow the user to store text as a document in iCloud. You might have noticed when we enabled iCloud that a ubiquity container was already created for you, as shown in Figure 15-27. In previous versions of Xcode, you would have had to add ubiquity containers; thankfully, you don’t need to anymore.

9781430259596_Fig15-27.jpg

Figure 15-27. The ubiquity container we created in Recipe 15-4

To get started, make a small change to the user interface. Open the Main.storyboard file and add a button, as shown in Figure 15-28, to the view controller. 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 if using a non-4” screen.

9781430259596_Fig15-28.jpg

Figure 15-28. 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 code in Listing 15-62.

Listing 15-62.  Adding a Protocol and Properties to the MyDocument.h File

//
//  MyDocument.h
//  Recipe 15-5 Storing UIDocuments in 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 (Listing 15-63).

Listing 15-63.  Implementing the contentsForType:error: Method

- (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 the implementation shown in Listing 15-64, the method also invokes the delegate to notify the view controller of the content change.

Listing 15-64.  Implementation of the loadFromContents:of Type: Method

-(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 Listing 15-65.

Listing 15-65.  The Complete ViewController.h File Implementation

//
//  ViewController.h
//  Recipe 15-5 Storing UIDocuments in 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

Next, go to the ViewController.m file. In the viewDidLoad method, add the bold line in Listing 15-66 to initiate an update of the Text View with the persisted text, if present.

Listing 15-66.  Add a Method Call to Update the Document

- (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, as shown in Listing 15-67.

Listing 15-67.  Starting Implementation of the updateDocument Method

- (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, as shown in Listing 15-68.

Listing 15-68.  The Finished updateDocument Method

- (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 whether the user logs out of iCloud or changes to a different account, add the implementation of the handleICloudDidChangeIdentity: notification method shown in Listing 15-69. It simply calls the updateDocument helper method.

Listing 15-69.  Implementing the handleICloudDidChangeIdentity: 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 might be called on an arbitrary thread, you need to make sure the updating is run on the main thread, as shown in Listing 15-70.

Listing 15-70.  Implementing the documentDidChange: Method

- (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 will be updated with the new text and saved to iCloud. To do that, add the implementation of the saveDocument: action method shown in Listing 15-71.

Listing 15-71.  Implementing 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 15-29.

9781430259596_Fig15-29.jpg

Figure 15-29. 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 about 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 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 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.

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

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