Talking to Services: iCloud and Dropbox - Pro iOS Persistence: Using Core Data (2014)

Pro iOS Persistence: Using Core Data (2014)

Chapter 8. Talking to Services: iCloud and Dropbox

With iOS 5, Apple introduced iCloud sync for Core Data. iOS developers rejoiced at the prospect of a drop-in solution for synching data among iPhone apps, iPad apps, and Mac OS X apps. Then they started using iCloud to sync Core Data data stores . . . and tears replaced the joy. Sync, as it turns out, is a hard problem to solve, and Apple hasn’t nailed it yet—at least for Core Data. As Apple iterates through the sync technology, however, it will likely get Core Data sync over iCloud working correctly in a future release of the SDK. So that you can have the information, we present the information for using iCloud Core Data sync in this chapter.

Other Core Data synching solutions have emerged, including libraries like ParcelKit (https://github.com/overcommitted/ParcelKit), Ensembles (www.ensembles.io/), and TICoreDataSync (http://timisted.github.io/TICoreDataSync/). We encourage you to investigate these solutions and refer to their respective documentation on their web sites, to see whether these are a better fit for you and your apps. We don’t replicate that documentation or cover all these libraries in this book. We do, however, explore the sync solution offered by Dropbox, as well as the aforementioned ParcelKit, which syncs Core Data over Dropbox.

Integrating with iCloud

While early versions of iCloud Core Data sync required some cerebral contortions to get anything to somewhat work, iOS 7 makes iCloud sync a lot simpler to manage. In this section, we build a simple app using one of Xcode’s templates and we use iCloud to sync data to the cloud. To get started, create a new project in Xcode using the Master-Detail Application template as shown in Figure 8-1.

image

Figure 8-1. Creating a master-detail application

On the next screen, name your app iCloudApp, select iPhone for the device and be sure to check the Use Core Data checkbox. At this point, we have a very simple app that uses a local Core Data store. You can launch the app to try it out.

Tip If you haven’t already done so, now is a good time to enable iCloud in the iOS simulator. In the iOS simulator, launch the Settings app and select the iCloud option. Enter your credentials and sign in.

Using iCloud’s Ubiquity Container

In this subsection, we tell iOS that the data store file needs to be saved in iCloud, in what’s called a ubiquity container. Open AppDelegate.m or AppDelegate.swift and look at the persistentStoreCoordinator method shown in Listing 8-1 (Objective-C) and Listing 8-2(Swift).

Listing 8-1. The Default persistentStoreCoordinator Method (Objective-C)
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it.
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}

// Create the coordinator and store

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"iCloudApp.sqlite"];
NSError *error = nil;
NSString *failureReason = @"There was an error creating or loading the application's saved data.";
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
// Report any error we got.
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data";
dict[NSLocalizedFailureReasonErrorKey] = failureReason;
dict[NSUnderlyingErrorKey] = error;
error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

return _persistentStoreCoordinator;
}

Listing 8-2. The Default persistentStoreCoordinator Method (Swift)
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("iCloudAppSwift.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

In order to add the integration with iCloud, we need tell Core Data that the store file needs to be synced. Update the method to pass an options dictionary to the addPersistentStoreWithType call, as shown in Listing 8-3 (Objective-C) or Listing 8-4 (Swift).

Listing 8-3. The persistentStoreCoordinator Method Updated for iCloud Sync (Objective-C)
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:@{ NSPersistentStoreUbiquitousContentNameKey : @"iCloudApp" } error:&error]) {

Listing 8-4. The persistentStoreCoordinator Method Updated for iCloud Sync (Swift)
let options = [NSPersistentStoreUbiquitousContentNameKey: "iCloudAppSwift"]
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: options, error: &error) == nil {

The only change is to set the NSPersistentStoreUbiquitousContentNameKey as an option of the persistent store coordinator. A ubiquity container is a component that automatically manages syncing files with iCloud. It’s a local representation of the iCloud data and sits on the file system outside your app’s sandbox. By setting this option, you tell Core Data that the persistent store file resides in a ubiquity container and is therefore synced with iCloud.

If you launched iCloudApp before, delete it from the iPhone Simulator so that any SQLite database files are removed. Then, launch the app. You’ll notice that not much has changed—it starts just normally. What is different, however, is the location of the data store file. If you open the Terminal application and navigate to the Documents folder of the app, you will notice that there is no iCloudApp.sqlite file.

Instead, there is a CoreDataUbiquitySupport directory that was created by iOS to attach your file to the ubiquity container. If you navigate through this directory, you will find your SQLite file buried deep in the file structure.

find "$HOME/Library/Developer/CoreSimulator" -name "iCloudApp.sqlite"

Synching Content

At this point, we have an application that saves its data store in iCloud. This is a great solution for keeping a backup of the data, but not so good if you are using your app on multiple devices. Unfortunately, the cloud will retain only the data saved by the last device you used, possibly overwriting what was saved by other devices you used.

In order for your code to synchronize data across devices, you subscribe to the notifications that Core Data sends for iCloud synching. Table 8-1 lists these notifications.

Table 8-1. Available Notification Types

Notification type

Description

NSPersistentStoreUbiquitousTransitionTypeAccountAdded

Sent when a new iCloud account was added while the app was running. This is useful to transition a store to the new account.

NSPersistentStoreUbiquitousTransitionTypeAccountRemoved

Sent when the existing iCloud account was removed from the device while the app was running. This tells you that the persistent store is transitioning to local storage.

NSPersistentStoreUbiquitousTransitionTypeContentRemoved

Sent when the user clears the contents of the iCloud account, usually using Delete All from Documents & Data in Settings. The Core Data integration will transition to an empty store file as a result of this event.

NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted

Sent when Core Data has finished building a store file that is consistent with the contents of the iCloud account.

You can make sure your app reacts to changes in iCloud in order to merge everything correctly by using the store notifications.

Open AppDelegate.m or AppDelegate.swift and add the appropriate notifications to the persistentStoreCoordinator method right after creating the persistent store, but right before adding it to the coordinator. The method should look like Listing 8-5 (Objective-C) orListing 8-6 (Swift).

Listing 8-5. Adding the iCloud Notifications to the persistentStoreCoordinator Method (Objective-C)
...
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"iCloudApp.sqlite"];
NSError *error = nil;
NSString *failureReason = @"There was an error creating or loading the application's saved data.";

// iCloud notification subscriptions
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(storeWillChange:) name:NSPersistentStoreCoordinatorStoresWillChangeNotification object:_persistentStoreCoordinator];
[notificationCenter addObserver:self selector:@selector(storeDidChange:) name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:_persistentStoreCoordinator];
[notificationCenter addObserver:self selector:@selector(storeDidImportUbiquitousContentChanges:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:_persistentStoreCoordinator];

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:@{ NSPersistentStoreUbiquitousContentNameKey : @"iCloudApp" } error:&error]) {
...

Listing 8-6. Adding the iCloud Notifications to the persistentStoreCoordinator Function (Swift)
...
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("iCloudAppSwift.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."

let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "storeWillChange:",
name: NSPersistentStoreCoordinatorStoresWillChangeNotification,
object: coordinator!)
notificationCenter.addObserver(self, selector: "storeDidChange:",
name: NSPersistentStoreCoordinatorStoresDidChangeNotification,
object: coordinator!)
notificationCenter.addObserver(self, selector: "storeDidImportUbiquitousContentChanges:",
name: NSPersistentStoreDidImportUbiquitousContentChangesNotification,
object: coordinator!)

let options = [NSPersistentStoreUbiquitousContentNameKey: "iCloudAppSwift"]
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: options, error: &error) == nil {
...

Of course, you must create the three new methods referred to by the notification selectors in order to remove compilation warnings (and ultimately runtime exceptions). Still in AppDelegate.m or AppDelegate.swift, add the methods shown in Listing 8-7 (Objective-C) or Listing 8-8 (Swift).

Listing 8-7. Handling iCloud Notifications (Objective-C)
- (void)storeDidImportUbiquitousContentChanges:(NSNotification*)notification {
}

- (void)storeWillChange:(NSNotification*)notification {
}

- (void)storeDidChange:(NSNotification*)notification {
}

Listing 8-8. Handling iCloud Notifications (Swift)
func storeDidImportUbiquitousContentChanges(notification: NSNotification) {
}

func storeWillChange(notification: NSNotification) {
}

func storeDidChange(notification: NSNotification) {
}

Leaving these methods empty won’t do much, of course. In the next section we fill out these methods to merge content from iCloud.

Merging Content from iCloud

When data change on another device that uses the same iCloud account as the device you are currently using, the app needs to know and be able to incorporate the new content from the cloud with its current content. Its current content might also include newly created content that iCloud doesn’t even know about yet. You merge changes between a device and iCloud in your handler for the NSPersistentStoreDidImportUbiquitousContentChangesNotification notification. Modify the storeDidImportUbiquitousContentChanges method as shown in Listing 8-9 (Objective-C) or Listing 8-10 (Swift) to do the merges.

Listing 8-9. The storeDidImportUbiquitousContentChanges Method Updated for iCloud Sync (Objective-C)
- (void)storeDidImportUbiquitousContentChanges:(NSNotification*)notification {
NSLog(@"%@", notification.userInfo.description);

NSManagedObjectContext *moc = self.managedObjectContext;
[moc performBlock:^{
// Merge the content
[moc mergeChangesFromContextDidSaveNotification:notification];
}];

dispatch_async(dispatch_get_main_queue(), ^{
// Refresh the UI here
});
}

Listing 8-10. The storeDidImportUbiquitousContentChanges Function Updated for iCloud Sync (Swift)
func storeDidImportUbiquitousContentChanges(notification: NSNotification) {
print("did import: \(notification.userInfo?.description)")

if let moc = self.managedObjectContext {
moc.performBlock({ () -> Void in
// Merge the content
moc.mergeChangesFromContextDidSaveNotification(notification)
})

dispatch_async(dispatch_get_main_queue()) {
// Refresh UI here
}
}
}

In this method, we simply call mergeChangesFromContextDidSaveNotification: on the managed object context and let Core Data deal with merging. We do this using the performBlock: method so that the change is done asynchronously on the context’s queue rather than on the main queue so as to not degrade the user interface responsiveness.

Finally, if necessary, we can refresh the user interface (UI). If your application is showing views that need to be modified because the data they are showing depends on the data that were changed, this is a good place to invoke a UI refresh.

Dealing with iCloud Account Changes

If your application is running while changes are made to the iCloud configuration—for example, if you enable iCloud—then you should handle the potential changes that the configuration changes may cause. To handle such changes, react to notifications indicating persistent store changes inside both storeWillChange: and storeDidChange:. Update the code in these methods as shown in Listing 8-11 (Objective-C) or Listing 8-12 (Swift).

Listing 8-11. Handling Changes to the iCloud Account (Objective-C)
- (void)storeWillChange:(NSNotification *)notification {
NSLog(@"%@", notification.userInfo.description);
NSManagedObjectContext *moc = self.managedObjectContext;
[moc performBlockAndWait:^{
NSError *error = nil;
if ([moc hasChanges]) {
[moc save:&error];
}

[moc reset];
}];

// This is a good place to let your UI know it needs to get ready
// to adjust to the change and deal with new data. This might include
// invalidating UI caches, reloading data, resetting views, etc...
}

- (void)storeDidChange:(NSNotification *)notification {
NSLog(@"%@", notification.userInfo.description);
// At this point it's official, the change has happened. Tell your
// user interface to refresh itself
dispatch_async(dispatch_get_main_queue(), ^{
// Refresh the UI here
});
}

Listing 8-12. Handling Changes to the iCloud Account (Swift)
func storeWillChange(notification: NSNotification) {
print("will change: \(notification.userInfo?.description)")

if let moc = self.managedObjectContext {
moc.performBlockAndWait({ () -> Void in
var error: NSError? = nil
if moc.hasChanges {
moc.save(&error)
}

moc.reset()
})
}
}

func storeDidChange(notification: NSNotification) {
print("did change: \(notification.userInfo?.description)")
// At this point it's official, the change has happened. Tell your
// user interface to refresh itself
dispatch_async(dispatch_get_main_queue()) {
NSFetchedResultsController.deleteCacheWithName("Master")
// Refresh UI here
}
}

It’s often better to prepare the UI for the change as early as possible to reduce the time it takes to actually apply the refresh. For this reason, the notifications are broken down into two phases you are probably already familiar with (change will happen and change did happen).

In our storeWillChange: implementation, we save the managed object context (if it has changes) and then reset its state, which discards all its managed objects. This is the point at which you’d prepare your UI to change.

In our storeDidChange: implementation, we asynchronously update the UI.

Your code is now ready to deal with iCloud integration. Before you can submit your application to Apple, you will need to add the iCloud entitlement to your app. You do this from within Xcode by selecting the project and going to the Capabilities tab. In that tab, you see a section for iCloud with a switch beside it. Turn on that switch, as shown in Figure 8-2, to add the iCloud entitlement to your app.

image

Figure 8-2. Adding the iCloud entitlement

Integrating with Dropbox Datastore API

With iOS, a lot of attention is often given to integrating with iCloud for persistence. Other solutions, however, might make sense for you depending on your app and your target audience. Dropbox has released its Datastore application programming interface (API), which allows you to use its cloud service as a ubiquitous data store. One of the obvious advantages of using Dropbox, rather than iCloud, is that Dropbox has APIs for multiple platforms. The Dropbox Datastore API doesn’t function like Core Data. It is not meant to act as a relational database where you create links between resources. Instead, it is a simple key/value pair store much like NoSQL databases such as MongoDB or Cassandra. One essential particularity is that is allows you to work without a schema, and therefore you need not worry so much about schema updates and migrations.

Setting Up the API Access

For iOS, the setup instructions are located at www.dropbox.com/developers/datastore/sdks/ios. The first step in setting up Dropbox with your app is to create a Dropbox app on the Dropbox web site. Log in to your Dropbox account and go towww.dropbox.com/developers/apps to create a new app. Select Dropbox API app and Datastores only, and choose a name for your app. For the name, be sure to not infringe on the Dropbox branding guidelines. In particular, don’t use “Dropbox” or something like it in the name of the app. The name must be unique across all Dropbox apps, so you’ll have to choose something other than “iOSPersistenceApp,” which is what we chose (see Figure 8-3).

image

Figure 8-3. Creating a Dropbox application

Once you create the app, the next screen shows you your app key and app secret, which you’ll need in the code for your app to be able to communicate with Dropbox. The app secret, as its name implies, should be kept secret.

We’re now ready to create our new application.

Creating a Blank App to Use with Dropbox

Since we’re now trying to use Dropbox as our persistence framework, we will leave Core Data alone for a while.

1. In Xcode, create a new Single View Application called DropboxPersistenceApp and don’t select the Core Data check box.

2. Once Xcode has created your app, add the Dropbox framework to the app. To do so, download it from Dropbox’s site (www.dropbox.com/developers/datastore/sdks/ios) and unzip the downloaded file to a temporary directory. Then, drag and dropDropbox.framework onto your Xcode project, below the Products folder.

3. In the sheet that appears, be sure to check Copy items if needed before clicking Finish.

4. Then, add to your project the other frameworks that the Dropbox SDK requires: CFNetwork.framework, Security.framework, SystemConfiguration.framework, QuartzCore.framework, libc++.dylib, and libz.dylib.

Your project structure should look like Figure 8-4.

image

Figure 8-4. Adding the Dropbox framework to the app

Configuring the Callback URL

For your app to authenticate against Dropbox, your app cedes control to the Dropbox authentication controller that the Dropbox SDK provides. This authentication controller shows the Dropbox login screen and, on successfully authenticating a user, returns control to your application by calling back via uniform resource locator (URL). Your app must register itself as a handler for that URL’s scheme, so that iOS can properly hand control back to your app.

To register your app as a handler for the URL scheme, go to Xcode, select your project under TARGETS, and go to the Info tab. Expand the URL Types section and press the + button to create a new URL type. In the URL Schemes field, enter db-APP_KEY, but replace APP_KEY with the app key Dropbox gave you when you created the app. It should look something like db-vp4jwudouxvhzz9. Leave the other fields as they are. When your app is installed, whether in the simulator or on an actual device, it will properly register itself.

Linking to Dropbox Using the Datastore API

For your code to use the Dropbox Datastore API, it must initialize the store manager, a DBStoreManager instance. A good place to perform this initialization is in your app delegate. Open AppDelegate.m if you’re building this project in Objective-C and add two import directives:

#import "TargetConditionals.h"
#import <Dropbox/Dropbox.h>

Then edit the application: didFinishLaunchingWithOptions: method to add the Dropbox Datastore initialization. Listing 8-13 shows the Objective-C version, and Listing 8-14 shows the Swift version.

Listing 8-13. Initializing the Dropbox Datastore API (Objective-C)
DBAccountManager *accountManager = [[DBAccountManager alloc]
initWithAppKey:@"APP_KEY" secret:@"APP_SECRET"];
[DBAccountManager setSharedManager:accountManager];

Listing 8-14. Initializing the Dropbox Datastore API (Swift)
let accountManager = DBAccountManager(appKey: "APP_KEY", secret: "APP_SECRET")
DBAccountManager.setSharedManager(accountManager)

Be sure to replace APP_KEY and APP_SECRET with the values given to you by Dropbox when you created your app on the Dropbox web site.

Once this configuration is done, we need to worry about letting the user link their Dropbox account through the app. We provide the mechanism for the user to link to Dropbox in the view controller to our app by doing the following:

1. In ViewController.h, (if you’re doing this in Objective-C) add the declaration of the method that will handle a tap to launch the Dropbox login window, as shown in Listing 8-15.

2. Implement that method in ViewController.m, as shown in Listing 8-16, or in ViewController.swift, as shown in Listing 8-17. Also, for the Objective-C version, add import statement in ViewController.m for "TargetConditionals.h" and<Dropbox/Dropbox.h>.

3. In Interface Builder, add a button with the label “Link to Dropbox” to the application’s view in Main.storyboard and connect it, through the Touch Up Inside event, to the didPressLink method.

Listing 8-15. Declaring the Event Handler in ViewController.h
- (IBAction)didPressLink;

Listing 8-16: Handling the Button Tap to Log In to Dropbox (Objective-C)
- (IBAction)didPressLink {
DBAccount *linkedAccount = [[DBAccountManager sharedManager] linkedAccount];
if (linkedAccount) {
NSLog(@"App already linked");
}
else {
[[DBAccountManager sharedManager] linkFromController:self];
}
}

Listing 8-17: Handling the Button Tap to Log In to Dropbox (Swift)
@IBAction func didPressLink(sender: AnyObject) {
if let linkedAccount = DBAccountManager.sharedManager().linkedAccount {
println("App already linked")
}
else {
DBAccountManager.sharedManager().linkFromController(self)
}
}

Now you are ready to launch the app. Once you do, you can tap the Link to Dropbox button and the Dropbox login screen appears. You can sign in with your own Dropbox credentials (note that this screen also handles two-factor authentication if you have that set in Dropbox), and control will return to our app. Unfortunately, our app doesn’t know quite what to do when control comes back from the Dropbox login screen. That is because the Dropbox login screen is performing a callback to our application, and that callback is done through the URL we set up earlier. We must instruct our app to handle the callback and open from that URL. In order to do this, implement application: openURL:sourceApplication:annotation: in AppDelegate.m or AppDelegate.swift, as shown in Listing 8-18 (Objective-C) or Listing 8-19 (Swift).

Listing 8-18. Handling the URL Callback in AppDelegate.m
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
DBAccount *account = [[DBAccountManager sharedManager] handleOpenURL:url];
if (account) {
NSLog(@"App linked successfully from %@", sourceApplication);
return YES;
}
return NO;
}

Listing 8-19. Handling the URL Callback in AppDelegate.swift
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String, annotation: AnyObject?) -> Bool {
let account = DBAccountManager.sharedManager().handleOpenURL(url)
if let account = account {
println("App linked successfully from \(sourceApplication)")
return true
}
return false
}

This is where you will want to update your UI to show that the Dropbox account is now linked and prepare the application to continue with Dropbox connectivity enabled. We do this in the next section.

Creating and Synching Data with the Dropbox API

Now that we’ve got our application linked to Dropbox, let’s do something with the Dropbox Persistence API. Declare a button property in ViewController.h for the Objective-C version, as shown in Listing 8-20, and then connect it in Interface Builder to the Link to Dropbox button. This way, we can alter its text appropriately to reflect the connection status to Dropbox.

Listing 8-20. Declaring the Button in ViewController.h
@property (weak, nonatomic) IBOutlet UIButton *theButton;

In AppDelegate.m or AppDelegate.swift, change the application: openURL:sourceApplication:annotation: method to look like Listing 8-21 (Objective-C) or Listing 8-22 (Swift) to set the button’s text to “Store” after successfully linking to Dropbox. For the Objective-C AppDelegate.m, add an import for ViewController.h.

Listing 8-21 Setting the Button’s Title to “Store” in AppDelegate.m
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
DBAccount *account = [[DBAccountManager sharedManager] handleOpenURL:url];
if (account) {
NSLog(@"App linked successfully from %@", sourceApplication);

ViewController *rvc = (ViewController*)self.window.rootViewController;
[rvc.theButton setTitle:@"Store" forState:UIControlStateNormal];

return YES;
}
return NO;
}

Listing 8-22. Setting the Button’s Title to “Store” in AppDelegate.swift
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String, annotation: AnyObject?) -> Bool {
let account = DBAccountManager.sharedManager().handleOpenURL(url)
if let account = account {
println("App linked successfully from \(sourceApplication)")

let controller = self.window?.rootViewController as? ViewController
controller?.theButton?.setTitle("Store", forState: .Normal)

return true
}
return false
}

Then, for consistency, edit the viewDidLoad method in ViewController.m or ViewController.swift to set the label of the button to the current link state, as shown in Listing 8-23 (Objective-C) or Listing 8-24 (Swift).

Listing 8-23. Setting the Button Text in ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];

if ([[DBAccountManager sharedManager] linkedAccount]) {
[self.theButton setTitle:@"Store" forState:UIControlStateNormal];
}
else {
[self.theButton setTitle:@"Link Dropbox" forState:UIControlStateNormal];
}
}

Listing 8-24. Setting the Button Text in ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()

if let linkedAccount = DBAccountManager.sharedManager().linkedAccount {
self.theButton?.setTitle("Store", forState: .Normal)
}
else {
self.theButton?.setTitle("Link Dropbox", forState: .Normal)
}
}

Of course, at this point we haven’t stored anything and if you run the app, link to Dropbox, and then press the “Store” button, all you will get is a message in the log saying the account is already linked. That is because in our event handling of the button touches, we don’t do anything else but try to link to Dropbox. So let’s change that.

In ViewController.m or ViewController.swift, add a private property that will contain a reference to our Dropbox data store, as shown in Listing 8-25 (Objective-C) or Listing 8-26 (Swift).

Listing 8-25. A Private Property for the Dropbox Data Store in ViewController.m
@interface ViewController ()
@property (nonatomic, strong) DBDatastore* datastore;
@end

Listing 8-26. A Property for the Dropbox Data Store in ViewController.swift
var datastore: DBDatastore?

The DBDatastore property represents the data container that will automatically be synced with Dropbox. As long as you keep an instance of it in your app, the data store will sync automatically.

Now you can edit the didPressLink: method to behave differently if the account is already linked, as shown in Listing 8-27 (Objective-C) or Listing 8-28 (Swift). As its comments describe, this updated method does the following:

· Creates a data store in Dropbox if one doesn’t already exist

· Gets a handle to the Notes table, creating it if necessary

· Creates a new note with the current date as its text and stores it in Dropbox

· Syncs the data store

Listing 8-27. Create a Note in Dropbox (Objective-C)
- (IBAction)didPressLink {
DBAccount *linkedAccount = [[DBAccountManager sharedManager] linkedAccount];
if (linkedAccount) {
// If there isn't a store yet, make one
if (!self.datastore) self.datastore = [DBDatastore openDefaultStoreForAccount:linkedAccount error:nil];

// Create or get a handle to the Notes table if it already exists
DBTable *notes = [self.datastore getTable:@"Notes"];

// Make a new note to store. In a normal app, this comes from typed text. To keep things
// simple, we just manufacture the text with a timestamp
NSDate *now = [NSDate date];
NSString *noteText = [NSDateFormatter localizedStringFromDate:now
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterFullStyle];

// Insert the new note into the table
[notes insert:@{ @"details": noteText, @"createDate": now, @"encrypted": @NO }];

NSLog(@"Inserted new note: %@", noteText);

// Make sure to tell Dropbox to sync the store
[self.datastore sync:nil];
}
else {
[[DBAccountManager sharedManager] linkFromController:self];
}
}

Listing 8-28. Create a Note in Dropbox (Swift)
@IBAction func didPressLink(sender: AnyObject) {
if let linkedAccount = DBAccountManager.sharedManager().linkedAccount {
// if there isn't a store yet, make one
if self.datastore == nil {
self.datastore = DBDatastore.openDefaultStoreForAccount(linkedAccount, error: nil)
}

// Create or get a handle to the Notes table if it already exists
let notes = datastore?.getTable("Notes")

// Make a new note to store. In a normal app, this comes from typed text. To keep things
// simple, we just manufacture the text with a timestamp
let now = NSDate()
let noteText = NSDateFormatter.localizedStringFromDate(now, dateStyle: .ShortStyle, timeStyle: .FullStyle)

// Insert the new note into the table
notes?.insert(["details": noteText, "createDate": now, "encrypted": false])

println("Inserted new note \(noteText)")

// Make sure to tell Dropbox to sync the store
datastore?.sync(nil)
}
else {
DBAccountManager.sharedManager().linkFromController(self)
}
}

At this point, you can run the app and after you’ve linked to Dropbox, every time you press the “Store” button, a new note is added into your data store. You can even verify that everything went well using Dropbox’s web view atwww.dropbox.com/developers/apps/datastores. You should see the app you created with a link to browse the data store. If you click the Browse link, you should see a listing of the notes you’ve created from the app.

Of course, you can always query the data store in your application code to get your records back. For example, to get all the notes in the Notes table, you’d write code like that shown in Listing 8-29 (Objective-C) or Listing 8-30 (Swift).

Listing 8-29. Retrieving All the Notes (Objective-C)
NSError *error;
DBTable *notes = [self.datastore getTable:@"Notes"];
NSArray *myNotes = [notes query:nil error:&error];

Listing 8-30. Retrieving All the Notes (Swift)
var error: DBError?
let notes = datastore?.getTable("Notes")
let myNotes = notes?.query(nil, error: &error)

Notice that the parameter we passed for query is nil, so the data store will return all records in the Notes table. The query parameter takes an NSDictionary (Objective-C) or AnyObject (Swift) instance to filter the result set it returns. We can filter the results by passing a dictionary object with the fields specified that we want to filter on. Suppose, for example, that some of the records we put into the Notes table are encrypted—that is, the dictionary passed to insert: has @"encrypted" set to @YES. To retrieve only those records, we’d use code like that shown inListing 8-31 (Objective-C) or Listing 8-32 (Swift).

Listing 8-31. Filtering the Result Set (Objective-C)
NSError *error;
DBTable *notes = [self.datastore getTable:@"Notes"];
NSArray *myNotes = [notes query:@{ @"encrypted": @YES } error:&error];

Listing 8-32. Filtering the Result Set (Swift)
var error: DBError?
let notes = datastore?.getTable("Notes")
let myNotes = notes?.query(["encrypted": true], error: &error)

You could, of course, set multiple values in the dictionary to filter on multiple keys.

Using Core Data with the Dropbox Datastore API: ParcelKit

We’ve seen how iCloud can be used to back up and sync Core Data stores. We’ve also seen how Dropbox’s Datastore API can be used as a schema-less store. In this section, we look at how the Datastore API can be used as a substitute for iCloud with your Core Data store. For this, we use the ParcelKit library (https://github.com/overcommitted/ParcelKit).

In order to illustrate the complete process, we start with a fresh application. In Xcode, create a new project using the Master-Detail Application template. Name the project DropboxEvents, and, this time, be sure to check the Use Core Data check box.

Of course, you may launch the application and get the regular Master-Detail sample application where you can hit the + button to create new events.

Adding the Required Frameworks for Dropbox and ParcelKit

You can add ParcelKit by using Cocoapods (http://cocoapods.org), or you can download the source from https://github.com/overcommitted/ParcelKit and follow the instructions on that page to build the framework. If you use cocoapods, you can skip the rest of this section.

In the previous section, we’ve seen how to integrate with Dropbox and add the frameworks it requires. For the DropboxEvents app, repeat those steps to add Dropbox.framework and the other required standard frameworks to the project.

· CFNetwork.framework

· Security.framework

· SystemConfiguration.framework

· QuartzCore.framework

· libc++.dylib

· libz.dylib

Next, add the ParcelKit library itself to your project. The dependencies should look as shown in Figure 8-5.

image

Figure 8-5. The dependencies of the Dropbox/ParcelKit application

As part of the ParcelKit configuration, go to the Build Settings of your project and add the –ObjC flag to the Other Linker Flags entry, as shown in Figure 8-6. Note that you do this for both the Objective-C and the Swift versions.

image

Figure 8-6. Other Linker Flags entry

Integrating DropboxEvents with Dropbox

As with the DropboxPersistenceApp application we built in the Dropbox Datastore API section, we must integrate DropboxEvents with Dropbox. Let’s go through it once more.

1. Open AppDelegate.m or AppDelegate.swift and edit the application: didFinishLaunchingWithOptions: method as shown in Listing 8-33 (Objective-C) or Listing 8-34 (Swift) to initialize the Dropbox SDK.

Listing 8-33. Initializing the Dropbox SDK (Objective-C)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

DBAccountManager *accountManager = [[DBAccountManager alloc]
initWithAppKey:@"APP_KEY" secret:@"APP_SECRET"];
[DBAccountManager setSharedManager:accountManager];

UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
MasterViewController *controller = (MasterViewController *)navigationController.topViewController;
controller.managedObjectContext = self.managedObjectContext;
return YES;
}

Listing 8-34. Initializing the Dropbox SDK (Swift)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let accountManager = DBAccountManager(appKey: "APP_KEY", secret: "APP_SECRET")
DBAccountManager.setSharedManager(accountManager)

// Override point for customization after application launch.
let navigationController = self.window!.rootViewController as UINavigationController
let controller = navigationController.topViewController as MasterViewController
controller.managedObjectContext = self.managedObjectContext
return true
}

2. Be sure to import "TargetConditionals.h" and <Dropbox/Dropbox.h> at the top of the AppDelegate.m, if applicable, and replace the app key and secret with the one you got from Dropbox. You can reuse the app key and secret you used for the DropboxPersistenceApp, or you can create a new app on Dropbox and get a new app key and secret.

3. You also must add, again in AppDelegate.m or AppDelegate.swift, the method necessary to handle the return from the Dropbox authentication controller, as shown in Listing 8-35 (Objective-C) or Listing 8-36 (Swift).

Listing 8-35. Handling the URL Callback (Objective-C)
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
DBAccount *account = [[DBAccountManager sharedManager] handleOpenURL:url];
if (account) {
NSLog(@"App linked successfully from %@", sourceApplication);

return YES;
}
return NO;
}

Listing 8-36. Handling the URL Callback (Swift)
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String, annotation: AnyObject?) -> Bool {
let account = DBAccountManager.sharedManager().handleOpenURL(url)
if let account = account {
println("App linked successfully from \(sourceApplication)")

return true
}
return false
}

4. To make sure the app is able to receive the callback from the Dropbox authentication controller, edit DropboxEvents-Info.plist and add the Dropbox URL type, as we did in the previous section and as illustrated in Figure 8-7. Again, use your actual app key from Dropbox, not “APP_KEY.”

image

Figure 8-7. The URL types

Once the setup work is done, we can start worrying about actually coding the app’s persistence with ParcelKit. Fortunately, ParcelKit does most of the hard work for us, and all we have to do is configure it through code.

1. The remainder of the work resides in MasterViewController.m or MasterViewController.swift, so let’s pop the appropriate file open. The first thing we need to do is disable the NSFetchedResultsController’s cache in order to prevent cache inconsistencies when receiving data from the cloud. This may feel a little weird, since normally you are supposed to provide a cache for NSFetchedResultsController for performance reasons, but data consistency trumps performance. Go to thefetchedResultsController method and change the cache name from "Master" to nil, as shown in Listing 8-37 (Objective-C) or Listing 8-38 (Swift).

Listing 8-37. Removing the Cache from fetchedResultsController (Objective-C)
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];

Listing 8-38. Removing the Cache from fetchedResultsController (Swift)
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)

2. Since we’re going to use Dropbox and ParcelKit, add the appropriate import statements at the top of the file for the Objective-C version, as shown in Listing 8-39.

Listing 8-39. Adding the Dropbox and ParcelKit Import Statements
#import "TargetConditionals.h"
#import <Dropbox/Dropbox.h>
#import <ParcelKit/ParcelKit.h>

3. Still in MasterViewController.m or MasterViewController.swift, add two properties to the private interface as shown in Listing 8-40 (Objective-C) or Listing 8-41 (Swift).

Listing 8-40. Adding Private Properties for the Data Store and Sync Manager (Objective-C)
@interface MasterViewController ()
@property (nonatomic, strong) DBDatastore *datastore;
@property (nonatomic, strong) PKSyncManager *syncManager;
@end

Listing 8-41. Adding Private Properties for the Data Store and Sync Manager (Swift)
var datastore: DBDatastore?
var syncManager: PKSyncManager?

4. Next we need to worry about the linking process. In our case, since everything relies on Dropbox being linked, we check for the link status when the master view appears—in the viewDidAppear: method. Add the code shown in Listing 8-42 (Objective-C) orListing 8-43 (Swift) to MasterViewController.m or MasterViewController.swift.

Listing 8-42. Checking the Dropbox Link Status When the View Appears (Objective-C)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

DBAccount *linkedAccount = [[DBAccountManager sharedManager] linkedAccount];
if (linkedAccount) {
if (!self.datastore) {
NSLog(@"Initializing datastore with ParcelKit sync");
self.datastore = [DBDatastore openDefaultStoreForAccount:linkedAccount error:nil];

PKSyncManager *syncManager = [[PKSyncManager alloc] initWithManagedObjectContext:self.managedObjectContext datastore:self.datastore];

[syncManager setTable:@"Events" forEntityName:@"Event"];
[syncManager startObserving];
self.syncManager = syncManager;
}
}
else {
NSLog(@"Needs link");
[[DBAccountManager sharedManager] linkFromController:self];
}
}

Listing 8-43. Checking the Dropbox Link Status When the View Appears (Swift)
override func viewDidAppear(animated: Bool) {
super.viewDidAppear()
if let linkedAccount = DBAccountManager.sharedManager().linkedAccount {
if datastore == nil {
println("Initializing datastore with ParcelKit sync")

self.datastore = DBDatastore.openDefaultStoreForAccount(linkedAccount, error: nil)

self.syncManager = PKSyncManager(managedObjectContext: self.managedObjectContext, datastore: datastore)

syncManager?.setTable("Events", forEntityName: "Event")
syncManager?.startObserving()
}
}
else {
println("Needs link")
DBAccountManager.sharedManager().linkFromController(self)
}
}

Let’s take a moment to examine the method’s logic. First, we grab the Dropbox linked account. If there isn’t any (i.e., it is nil), then we create one and we launch the linking process.

If the app already has a linked account, then we use it. We set up the Dropbox Datastore exactly in the same manner as we explained in the previous section. We then create an instance of ParcelKit’s PKSyncManager, which is in charge of observing changes to Core Data’s managed objects and synching them with Dropbox, and also observing changes to object in Dropbox and synching them with Core Data’s managed objects. To perform this magic, ParcelKit requires us to tell it which Core Data entities we want to observe. In this case, there is only one: the Event entity, which we want to link with the Events table in the Dropbox Datastore. Note that though we’ve chosen to match the name of the Core Data entity with the Dropbox Datastore table name, they don’t have to match. PKSyncManager’s setTable:forEntityName: allows you to map the two using whatever names you choose.

Once the PKSyncManager instance is fully set up, we must assign it to the syncManager property in order to keep it alive. Otherwise, it would fall out of scope and be destroyed.

That’s it for all the code we need to write. See, we told you that ParcelKit does all the hard work! There is, however, one last bit we need to put in place in order to make all this work. ParcelKit requires an additional attribute called syncID to be set on each entity it observes. This is the key it uses to compare the different instances of the entity and ensure that it keeps everything synced correctly. Open DropboxEvents.xcdatamodeld and add a syncID attribute of type String to the Event entity, as shown in Figure 8-8.

image

Figure 8-8. The syncID attribute set for ParcelKit

Since we’ve modified the Core Data model, you will need to delete the existing Core Data data store from the simulator to avoid Core Data model collision. In the iPhone simulator, press and hold on the app’s icon. Once it wiggles, press the delete button to uninstall it. Then press the home button to exit this mode.

This time, we’re all set. Launch the application and notice how nothing different seems to be happening. Go ahead and add a couple more events. They add without any problem. To notice a difference, we need to log into Dropbox’s web view atwww.dropbox.com/developers/apps/datastores. Browse to your data store and you should see the events you just created, as illustrated by Figure 8-9.

image

Figure 8-9. Dropbox’s Datastore web view

As you add new events in your app, new records appear in the data store. Now that you’ve created a few events, stop the application. Following the same procedure as before, uninstall the app from the iPhone simulator. This will erase the local Core Data store. With a regular Core Data application, if you launched the app again you’d have no events. Go ahead and launch the app and notice how all the events are still there. That is because they have been synced with Dropbox’s Datastore API.

Conclusion

Nowadays, most consumers own multiple devices and they expect their data to be omnipresent across those devices. We’ve seen examples of three options for storing your data on the cloud and mentioned two more. Many more cloud solutions exist, ranging from standard services like the ones we’ve discussed in this chapter to custom services that you may develop yourself. No matter which solution you choose, the evolution of data storage undeniably leads to a need for cloud storage. People will be more likely to download and use your apps if you take care of syncing across devices. The decision between iCloud, Dropbox, or other is one that you need to make based on your target users, the characteristics specific to your app, and whether or not you want to sync data with apps running on non-Apple devices.