Using Key-Value Coding (KVC) - Using the Technologies - Learning iCloud Data Management (2014)

Learning iCloud Data Management (2014)

Part III: Using the Technologies

8. Using Key-Value Coding (KVC)

With this chapter, you write your first iCloud apps. You’ve used iCloud data in apps from Part II of this book, but iCloud stayed in the background there. Now, it’s time to work directly with iCloud. Not only do you work directly with iCloud, but you do it twice: once on iOS and once on Mac OS X. This is your first pair of Round Trip apps.

There are two types of storage in iCloud: document storage and key-value (KVC) storage. KVC is the simpler of the two, and it is used in many, if not all, apps. (Apple recommends that it be enabled in all apps submitted to the App Store.)

KVC uses simple data types such as numbers, strings, dates, and so forth. It is limited to 1 MB per app on a user’s device. It is best suited for data such as preferences and configuration. In part because of its lightweight storage, it requires less attention than documents and other more complex data stores.

In this part of the book, you’ll move from the simplicity of KVC on to property lists, Core Data (the most powerful data store), and specialized types of data stores such as resources in app bundles. By the end of Part III, you should be familiar with all of the iOS and OS X data storage mechanisms. Furthermore, in Part III and into Part IV, you’ll see how to use them with iCloud either directly (as is the case with KVC), in documents (the case with Core Data), and in file wrappers that combine several store types.


Note

The code shown in this chapter (and downloadable as described in the Preface) is based on the Utility Application (iOS) and Cocoa Application (OS X). It uses key-value coding to store a text string in iCloud. It also stores the current value in local settings on each device. Normally, the most recent value is the best value for synchronization, but there are cases in which older values are better to use. Sometimes, you can write code to automatically choose among conflicting values, and sometimes you will need to ask the user. That code depends on your app, so its location is pointed out here, and you can implement whatever conflict resolution you choose.


Setting Up a Controlled Testing Environment

As you work with KVC, you’ll see how to synchronize data across a user’s devices. Because KVC is designed for small amounts of data, you’ll find that it is always available to a user. Where there are data conflicts in synchronization, that solution is quite simple: the latest value wins. The user is usually not involved.

You’ll see how you can enter data on one device and have it appear on another device automatically. In Round Trip solutions, those devices can be running both on iOS and OS X. The time lag for iCloud updates and synchronization is noticeable: you are not dealing with a direct connection such as when you are updating a local document or even a web page. Particularly as you start to test what happens as data automatically moves from your iPhone to your Mac and to your iPad (along with your modifications to it), it is remarkably easy to get confused. In fact, it is almost impossible not to. Precisely because of the time lag, if you blink, you may miss the moment of the update. You may start by looking at two screens: one has Test 1 on the screen and the other has Test 2 on the screen (or whatever you want to type in). Blink, look away for a moment, or take a sip of coffee, and they may both have the same text on their screens. Continue with your testing by typing something new into one device and repeat the process. Before very long, you’ll find yourself confused about which device received the update and which generated it.

Of course, you may say, it’s easy to be organized and keep track of where you are entering data. However, that doesn’t solve the problem: much of your testing of synchronization must rely on not repeating patterns. In a very controlled testing environment, you can create a list of synchronization tests to perform, but for many developers (particularly independent developers without a support staff), this is impractical. One way in which you can help yourself keep track of synchronization testing is to use a set of cards to remind you of what data has been entered on what devices.

Thus, consider purchasing four packs of colored cards (3- × 5-inch cards are fine for this purpose). (Know that this is not a craft project even though you’re buying materials often used in that way.) Line up your devices on your desk and write down the initial values you’re setting on each one (if any) using different colors. As the values change, just move the cards. The different colors help you keep track.

As noted, there is a definite lag in the synchronization process, but the problem arises particularly when you’re trying to cause collisions and conflicts (an issue that affects other storage techniques more so than KVC). You need a way to quickly change data and keep a record of what the values were before it was changed. The colored card technique may work for you: it works for many developers.

Implementing KVC

KVC is implemented with an informal protocol, NSKeyValueProtocol. Default implementations are provided by NSObject. The elements of KVC are keys and values—hence the name. The keys are NSString objects (they begin with a lowercase letter and contain no spaces), and the values are of type id. Thus, the KVC design pattern is powerful and flexible because it can accommodate any kind of Objective-C class.

For iCloud, KVC is implemented in a limited way—in fact, it is implemented with the same limits as property lists, the subject of Chapter 9, “Using Preferences, Settings, and Keychains with iCloud.” The objects that can be used with iCloud KVC are simple objects including

Image NSNumber

Image NSDate

Image NSString

Image NSData

Image NSArray

Image NSDictionary

NSArray and NSDictionary objects themselves can contain any of these objects, so you can construct complex structures. Before you do so, however, bear in mind that for a given app on a given user’s device, there are limits to the available space. (Check on developer.apple.com for the latest values: storage is getting faster and devices are gradually getting more of it.)

iCloud synchronization of key-value data succeeds or fails atomically. Keep that in mind as you design your data. If you store multiple values in an NSArray or NSDictionary all—or none—of them will be synchronized at the same time through iCloud.

KVC as implemented for iCloud lends itself to preferences and settings. If you look at the full documentation of KVC, you’ll see that it can handle to-many relationships to groups of properties. For all intents and purposes, the iCloud implementation of KVC is a to-one representation, such as the single value for a user’s highest score in a game or the user’s player name in a game. (KVC is not designed for secure storage of passwords. For a discussion of secure storage in the context of iCloud, see Chapter 9, “Using Preferences, Settings, and Keychains with iCloud.”)

KVC has become a major component of AppleScript support in OS X. It can provide AppleScript support with minimal effort on the developer’s part.

Following are the two primary methods of the protocol that implements KVC:

- (void)setValue:(id)value forKey:(NSString *)key
- (id) value: forKey:(NSString *)key

Testing iCloud on iOS Simulator

Until Xcode 5, iCloud was one of the technologies that couldn’t be tested on iOS Simulator. It is now implemented there. You use it just as you would use it on an iOS device. On iOS Simulator, go to Settings and set up your iCloud testing account. To properly test, you should be using that account on at least one iOS device and also on a Mac.

The one difference from deployment on a device is that on iOS Simulator, there are no automatic syncs. Once you have logged into iCloud using Settings on your simulator, choose Trigger iCloud Sync from the Debug menu to force a sync.

Preparing Your Project for Testing

Begin by creating your projects. As noted previously, they will be based on Utility Application (iOS) and Cocoa Application (OS X). On iOS, create a universal app. Make certain that at the very least you have one OS X device and one iOS device ready to be used for testing. (It’s better to have both an iPad and iPhone for iOS testing.) Do not use the option for Core Data (that is covered in Chapter 10, “Managing Persistent Storage with Core Data”).

If you haven’t read Chapter 2, “Setting Up iCloud for Development,” review the steps in that chapter. You’ll need them in order to proceed. As pointed out there, it is usually best to create your signing identities and certificates on developer.apple.com and set up your devices there. Use Xcode 5 to add capabilities to your app ID, and to manage your profiles.

Here’s a quick summary of the steps to take:

1. Create the iOS and OS X projects.

2. Before making any changes, run them to make certain they run. If they don’t run, don’t expect magic to happen. Recreate the projects and try again. If you’re still in trouble, you may need to spend one of your DTS (Developer Technical Support) incidents to resolve the matter: it will be worth it.

3. Select the project and go to the Capabilities tab, as shown in Figure 8.1.

Image

Figure 8.1 Opening Capabilities

4. Begin to turn on iCloud. You may be asked to choose which development team you have listed in your Xcode Preferences under Accounts you want to use, as shown in Figure 8.2.

Image

Figure 8.2 Selecting a development team

Xcode will turn on iCloud as shown in Figure 8.3. If it encounters errors, it will give you the option to have them fixed automatically.

Image

Figure 8.3 Turning on iCloud

5. Turn on the checkbox for the key-value store.

Xcode may fill in the ubiquity container name. You don’t need it for key-value coding.

6. Run the project again. You should have no problems—you’re not actually using iCloud, but the permissions should be set up properly at this point.

7. Repeat these steps to create an OS X project.

Now add the code in the listings in this chapter. Note that because some of the code is interrelated, until all of it is added (there’s not that much), the project won’t build and run.

Sharing the Key-Value Store for the Round Trip

In order to implement the Round Trip, both your iOS and OS X apps need to share the same key-value store. When you turn on iCloud, an entitlements file is created for you (you can find it in the Project navigator). It will have a dictionary called Entitlements File. Find the key callediCloud Key-Value Store (it may be the only key in the dictionary if you’ve just created the project).

By default, this key is set to the following value:

$(TeamIdentifierForPrefix)$CFBundleIdentifier)

This concatenates the team ID with the app’s bundle identifier, but you can change it if you want to—and you do want to for a shared store. Take one of your apps (either the iOS app or the OS X app) and locate the bundle identifier on the General tab of the project. In that location, you will be able to see the bundle identifier. It will typically be in the following form:

com.champlainarts.iOS-KVC-iCloud

That is what $CFBundleIdentifier is set to now. Leave the bundle identifier as it is in that project, but copy it from the bundle identifier field.

Now, go to the entitlements file in the other project and find the iCloud key-value store. Change it from its default value to a shared value. This means that instead of something like

$(TeamIdentifierForPrefix)$(CFBundleIdentifier)

you will have something like this in the second file:

$(TeamIdentifierForPrefix)com.champlainarts.iOS-KVC-iCloud

If you prefer, you can explicitly set the store in both projects, but it’s only necessary to change the default values in one of the projects to match the other. (You don’t want to change the bundle identifier; just change the iCloud Key-Value Store value.)

Setting Up and Using NSUbiquitousKeyValueStore

In your app, you interact with NSUbiquitousKeyValueStore. This is the KVC for your app and the user. Each key that you use is unique to this combination of app and user, and that’s why you are limited to small amounts of data. However, one of the types that you can store is anNSDictionary. Thus, you can build in a second level of data with a dictionary. (In fact, you can go several levels of data deep as long as you stay within the 1 MB limit.)

Looking at the Methods

If you explore the class reference for NSUbiquitousKeyValueStore, you’ll see that there are six groups of methods (most of the methods have only one method):

Image There is a class method that returns the default store. You almost always use it as shown in the code that follows:

+ (NSUbiquitousKeyValueStore *)defaultStore

Image You can explicitly synchronize your local disk store (maintained automatically) with your in-memory data using the following code. You should do this when your app launches and when it returns to the foreground. As part of the sync process, iCloud is informed of new data, but it may not update the local data at that time. Don’t call this method frequently because it’s unnecessary and can slow down your app.

- (BOOL)synchronize

Image You can remove an object for a key with the following code. You may want to synchronize the data after you do so, depending on your app and what the data involved is.

- (void)removeObjectForKey:(NSString *)aKey

Image There is a set of eight getters for the eight types of values that can be stored (NSArray, BOOL, NSData, NSDictionary, double, long long, id, and NSString). You use them as needed. They all share the same syntax:

- (NSArray *)arrayForKey:(NSString *)aKey

Image There is a corresponding set of eight setters for the same eight types. You use them as needed, as follows:

- (void)setArray:(NSArray *)anArray forKey:(NSString *)aKey

Image Finally, you can convert all keys and data in the store to a dictionary for further processing (or debugging), as follows:

- (NSDictionary *)dictionaryRepresentation

Working with the Store

There are four basic interactions that you have with the NSUbiquitousKeyValueStore:

Image Preparing the User Interface: These are standard Cocoa and Cocoa Touch techniques to add interface elements and connect them to your code.

Image Setting Up the Store at Runtime: These are the steps you perform when your app launches.

Image Monitoring Store Changes: As you saw in Chapter 7, you use notifications to keep track of store changes and update interface items.

Image Monitoring Interface Changes: In addition to updating the interface from store changes, you need to update the store in the other direction from interface changes.

Preparing the User Interface

The user interfaces of these apps are very simple so you can focus on the iCloud functionality. Each has a text field into which you can type data. On iOS, this field has a delegate that implements textFieldDidEndEditing:. In order to fire that message, there is a second text field on iOS that does nothing itself: it just serves as a field you can tap to force the main text field to send textFieldDidEndEditing:.

On OS X, instead of a delegate there is a Save button. It is wired to an action called saveData:. The action as well as the delegate execute the same code. They move the value out of the text field into a local property (_editedValue). Then, they store that value both in local user defaults or settings and in the iCloud key-value store. This means that in the event that iCloud is unavailable, there is a local value for the next time the app runs.

There is another difference between the two apps. On OS X, the setup for iCloud is done in the app delegate; it also manages notifications and updates. Furthermore, in this example, the app delegate manages the text field and button. In a production app, a window controller or other object handles all of these interface elements and tasks, but this app is simplified down to the iCloud basics, so the window controller is not involved.

In the iOS app, the view controller handles the text fields, and it receives iCloud notifications. It is the view controller that then manages updates from iCloud data inside the view controller. In order to make that code a little clearer, the setup of iCloud is also done in the view controller. Just as with the OS X sample, in a production version it is likely that the iCloud setup and notification receiving would be done in the app delegate and the interface elements would be handled by a view controller.

On iOS, add two editable text fields to the storyboard in the main view controller. Wire one of them up to the class extension in MainViewController.m (in the Assistant, control-drag from a text field to the class extension). The other one isn’t wired up to anything: it’s simply there so that you can tap it to force the main field to stop editing. Do this for both the iPhone and the iPad storyboards. In the storyboard, link the main text field’s delegate to MainViewController. The new properties are shown in Listing 8.1. Manually add the editedValue property—it will contain the value that is shown in the field.

Listing 8.1 New Declarations in MainViewController.m on iOS


#import "MainViewController.h"

static NSString* kEditableFieldKey = @"editableField"; //1

@interface MainViewController ()

@property (weak, nonatomic) IBOutlet UITextField *editableField; //2
@property NSString *editedValue; //3

@end


1 The static NSString is the key you will use.

2 editableField is a text field you add to your storyboard. Make sure to connect it to the declared property here.

3 editedValue is the value that is entered or retrieved from iCloud.

Listing 8.2 shows the corresponding code on OS X. Note that the only difference is that this is in the app delegate rather than in a view controller. Add a text field to MainMenu.xib along with a button. Wire up the text field to the class extension in the app delegate, and wire the button to a new action in app delegate that you can call saveData. Listing 8.2 shows the new properties in the app delegate.

Listing 8.2 New Declarations in AppDelegate.m on OS X


#import "JFAppDelegate.h"

static NSString* kEditableFieldKey = @"editableField"; //1

@interface JFAppDelegate ()

@property NSString *editedValue; //2
@property (weak) IBOutlet NSTextField *editableField; //3

@end


1 The static NSString is the key you will use.

2 editedValue is the value that is entered or retrieved from iCloud.

3 editableField is a text field you add to your storyboard. Make sure to connect it to the declared property here.

Setting Up the Store at Runtime

Early in your app’s processing, you set up the store; you only do it once when the app runs. Depending on the structure of your app, the precise location in your code may vary, but it should be before any user input is permitted. (In the listings that follow, some comments from the templates are included to help you find the right location to insert the code.)

Listing 8.3 shows the code on iOS (based on the Utility application template). The notification fires whenever changes are made to the store in the future. As noted previously, to simplify the code, it is placed in MainViewController.m rather than in AppDelegate.m.

Listing 8.3 Setting Up the Store and UI on iOS


- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(updateCloudItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:[NSUbiquitousKeyValueStore defaultStore]]; //4

[[NUbiquitousKeyValueStore defaultStore] synchronize]; //5

_editedValue = [[NSUbiquitousKeyValueStore defaultStore]
stringForKey:kEditableFieldKey]; //6

_editedValue = [[NSUserDefaults standardUserDefaults]
stringForKey: kEditableField]; //7

_editableField.text = _editedValue; //8
}


4 updateCloudItems is the name of the method you will write. Everything else is usually boilerplate.

5 Synchronize the store to catch changes made since the last time you ran. You don’t need to sync continually.

6 Overlay the local value with the value from iCloud. A breakpoint on this line for debugging is also a good idea.

7 Retrieve the value from the local user defaults. This is a good line on which to place a breakpoint to observe the local value.

8 Depending on your app, you may place the value in a UI element. Note that this syntax varies between iOS and OS X. UITextField uses the text property starting in iOS 6. Compare the syntax to line 13 in Listing 8.4. This is a difference between Cocoa and Cocoa Touch.

On OS X, the corresponding code often goes into AppDelegate.h in the applicatonDidFinishLaunching: method. Listing 8.4 shows the code for AppDelegate.m on OS X.

Listing 8.4 OS X Code to Initialize the Key-Value Store


- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
// Insert code here to initialize your application

[[NSNotificationCenter defaultCenter]
addObserver: self
selector: @selector (updateCloudItems:) //9
name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object: [NSUbiquitousKeyValueStore defaultStore]];
[[NSUbiquitousKeyValueStore defaultStore] synchronize]; //10

editedValue = [[NSUserDefaults standardUserDefaults]
stringForKey: kEditableField]; //11

_editedValue = [[NSUbiquitousKeyValueStore defaultStore]
stringForKey:kEditableField]; //12

[_editableField setStringValue: _editedValue]; //13
}


9 updateCloudItems is the name of the method you will write. Everything else is usually boilerplate.

10 Synchronize the store to catch changes made since the last time you ran. You don’t need to sync frequently.

11 Retrieve the value from the local value. This is a good line on which to place a breakpoint to observe the local value.

12 Overlay the local value with the value from iCloud. (You can elaborate on this to compare the two; you may also choose to use the iCloud value. This is safer with KVC than with ubiquity storage containers for documents.) A breakpoint on this line for debugging is also a good idea.

13 Depending on your app, you may place the value in a UI element. NSTextField in OS X uses setString. Compare this syntax to line 8 in Listing 8.3. This is a difference between Cocoa and Cocoa Touch.

Monitoring Store Changes

Monitoring changes in the key-value store is the same in both versions. Listing 8.5 shows the code. This method is called from the notification that you registered to observe in Listing 8.3 (iOS) or 8.4 (OS X). Both of them register updateCloudItems as the selector to be called with the notification. Here is the code that will run at that time. You use this code without alteration most of the time, except for lines 6 through 9. The rest of the code simply unpacks the notification and loops through the changed keys. You test for whatever keys you care about. In this case, an ifstatement is used, but you could use a switch statement.

In the example code, this goes in AppDelegate.m on OS X and in MainViewController on iOS.

Listing 8.5 Updating Data from iCloud on iOS and OS X


#pragma mark - Cloud support

- (void)updateCloudItems:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo]; //1

NSNumber* reasonForChange = [userInfo
objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey]; //2
if (reasonForChange) //3
{
NSInteger reason = [[userInfo
objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey]
integerValue];
if (reason == NSUbiquitousKeyValueStoreServerChange ||
reason == NSUbiquitousKeyValueStoreInitialSyncChange)
{
NSArray *changedKeys = [userInfo
objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; //4

for (NSString *changedKey in changedKeys) //5
{
if ([changedKey isEqualToString:kEditableFieldKey]) //6
{
editedValue = [[NSUbiquitousKeyValueStore defaultStore]
objectForKey:kEditableFieldKey]; //7
[[NSUserDefaults standardUserDefaults]
setObject:_editedValue forKey:kEditableFieldKey]; //8
//ONLY USE ONE OF THE FOLLOWING
[_editableField setStringValue: _editedValue]; //9
_editableField.text = _editedValue //10

}
}
}
}
}


1 Unpack the userInfo dictionary from the notification.

2 Get the reason for the change.

3 There may not be a reason, so test for it.

4 Move the changed keys into a local array.

5 Fast enumerate through the keys.

6 Check to see if this is a key you care about (customize this line of code).

7 Store the changed value in a local variable or property (customize this line of code).

8 Store the value in user defaults (if relevant, customize this line of code).

9, 10 Use only one of these. In iOS 6
and later, UITextField uses the text property. NSTextField on OS X uses the setStringValue accessor.

Monitoring Interface Changes

You have to be able to go in the other direction—from the user interface to the store. This is done by setting the delegate for the text field to one of your classes if you use the delegate structure. If you use a button to save changes, you wire the button up to an action. In this section, you will see how to do both, and you will also see that whether you use a delegate or a button to end editing, the same code will need to be invoked. Note that although one technique is used in this example for OS X and the other for iOS, either technique can be used for either operating system.

If you use the delegate structure as in the iOS code here, add the UITextFieldDelegate to the MainViewController declaration as shown in Listing 8.6.

Listing 8.6 Adopting the UITextFieldDelegate Protocol on iOS


#import "FlipsideViewController.h"

@interface MainViewController : UIViewController
<FlipsideViewControllerDelegate,
UIPopoverControllerDelegate, UITextFieldDelegate>

@property (strong, nonatomic)
UIPopoverController *flipsidePopoverController;

@end


In the storyboard, set the delegate of the first text edit field to the Main View Controller.

If you use the button structure as in OS X in this example, add the button to the MainMenu.xib file and wire it up to a new action in the app delegate (you can call it saveData).

Now implement either (or both) the saveData method and the textFieldDidEndEditing: method.

Listing 8.7 shows the saveData method that will be wired up to the Save button if you use it.

Listing 8.7 Storing Data with a Save Button on OS X


- (IBAction)saveData:(id)sender {
_editedValue = [_editableField stringValue]; //11
[[NSUserDefaults standardUserDefaults]
setObject:_editedValue
forKey:kEditableFieldKey]; //12
[[NSUbiquitousKeyValueStore defaultStore]
setString: _editedValue
forKey:kEditableFieldKey]; //13

}


11 Take the value from the text field and store it in _editedValue.

12 Store the value in standardUserDefaults locally.

13 Store the value in the ubiquitous key-value store.

On iOS, you can use a delegate of the text field to save the data, as shown in Listing 8.8.

Listing 8.8 Storing Data with a Delegate on iOS


#pragma mark - textfield delegate
- (void)textFieldDidEndEditing:(UITextField *)textField {
self.editedValue = [textField text]; //14
[[NSUserDefaults standardUserDefaults] setObject:self.editedValue
forKey:kEditableFieldKey]; //15
[[NSUbiquitousKeyValueStore defaultStore]setString: self.editedValue
forKey: kEditableFieldKey]; //16
}


14 Take the value from the text field and store it in self.editedValue.

15 Store the value in standardUserDefaults locally.

16 Store the value in the ubiquitous key-value store.

Chapter Summary

Key-value storage in iCloud is the simplest use of iCloud. The data consists of tuples—a key and a value. Because it is simple data, it’s used often for settings and preferences; it also is always there. In this chapter, you have seen how to set up key-value stores in iCloud and how to use them in OS X and iOS. You can change values on your iPhone and watch them appear on your iPad—and on your Mac (and, of course, any other sequence or combination).

Exercises

1. Work with your apps to get a better feel of the latency in iCloud updates. Turn off WiFi in your devices (turn on Airplane mode on iOS), and make changes. Then, as quickly as possible, reverse the settings and watch the changes appear.

2. Make a list of the kinds of settings and preferences that would require you to involve the user to resolve inconsistencies. Sometimes the wording of the setting or preference can minimize inconsistencies by pushing the user to thinking very specifically about the setting or preference. (As an example, consider replacing a high/low security setting with explicit settings that govern different values.)

3. In the sample apps for this chapter, you synchronized a single field and its data. Add another field, which is to be the title of the single field. Both should be synced together (title and value). Experiment with multiple devices to see if you can get the title and the value out of sync. (Hint: The next chapter shows you how to prevent this.)