iOS Tutorial - nothirst/TICoreDataSync GitHub Wiki

Implementing TICoreDataSync in an iPhone, iPad or iPod touch Core Data application

This tutorial walks through adding the TICoreDataSync framework to a very simple iOS application.

The example app uses Dropbox sync via TICoreDataSync’s built in DropboxSDK-based sync classes. The example app’s user interface is deliberately basic, “designed” to demonstrate the framework with minimal distractions.

This is the iOS equivalent of the Mac app developed in the Mac Tutorial.

The iOSNotebook Application

The iOSNotebook application stores notes, which can be assigned tags:

Notebook sample app screenshot

The GitHub repository includes a vanilla version of the app (excluding any sync code) in Examples/Tutorial/iOSNotebook. Alternatively, feel free to use your own application.

The finished version is the same as the iOSNotebook example app, which can be found in Examples/iOSNotebook/.

Adding Necessary Files

As with the Mac tutorial, the first step is to add the TICoreDataSync-iOS.xcodeproj to the project, along with the Cocoa frameworks you’ll need later.

Add the TICoreDataSync-iOS.xcodeproj:

Right-click on the project, choose Add Files to “iOSNotebook”…, then choose the TICoreDataSync-iOS.xcodeproj project file from the TICoreDataSync directory (at the same level as the Examples directory):

Adding the TICoreDataSync-iOS Xcode project to the Notebook project

Add the TICoreDataSync Supporting Files

The TICoreDataSync project includes a handful of supporting files that you will need to add directly to your project. Right-click on the project, choose Add Files to “iOSNotebook”…, and add the following files:

  1. TICoreDataSync/03 Internal Data Model/TICDSSyncChange.xcdatamodel
  2. TICoreDataSync/03 Internal Data Model/TICDSSyncChangeSet.xcdatamodeld
  3. TICoreDataSync/05 File Structure/deviceInfo.plist
  4. TICoreDataSync/05 File Structure/documentInfo.plist
  5. TICoreDataSync/05 File Structure/ReadMe.txt

Add the TICoreDataSync Supporting Files

Add the required frameworks and libraries:

To add a framework using Xcode 4, click the iOSNotebook project icon in the Project Navigator (⌘-1), select the iOSNotebook target, then the Summary tab, and click the + button under the Linked Frameworks and Libraries list:

Add the Security.framework, which is needed by the encryption code. Add QuartzCore.framework. Then add libz.dylib which is used by SSZipArchive to compress the whole store. Lastly add libTICoreDataSync-iOS.a.

Adding frameworks and libraries to the Notebook project

Add the target dependency

Prior to building the iOSNotebook target the TICoreDataSync-iOS target needs to be built. Select the iOSNotebook target, then the Build Phases tab. Add TICoreDataSync-iOS under the Target Dependencies section.

Adding target dependencies

Update the Header Search Paths

The iOSNotebook project needs to know where to look for the TICoreDataSync header files. Select the iOSNotebook target, then the Build Settings tab and scroll down to the Header Search Paths entry. Double click to edit it and add a recursive entry for "$(SRCROOT)/../../..". This path assumes you've left the iOSNotebook project in the Examples/Tutorial directory.

Updating the Header Search Paths

Update the Other Linker Flags

The TICoreDataSync static library declares some categories on different Objective-C classes. To ensure that the iOSNotebook application can see these categories you'll need to add the -ObjC flag to the Other Linker Flags sections of iOSNotebook's Build Settings. Select the iOSNotebook target, then the Build Settings tab and scroll down to the Other Linker Flags entry. Double click to edit it and add an entry for -ObjC.

Updating the Other Linker Flags

Managed Object Requirements

TICoreDataSync uses the nested managed object context feature that was added to Core Data in 10.7/iOS 5 (Core Data Release Notes for OS X v10.7 and iOS 5.0). As such, your application's main context must be of type NSMainQueueConcurrencyType. Additionally, in order for changes to be recognized, every managed object you wish to synchronize must be an instance of TICDSSynchronizedManagedObject and have a ticdsSyncID attribute.

(Optional) Add a Data Model Version

If you've run iOSNotebook once already at this point it will have created a version of the data store that is incompatible with the changes you will make below. If you run it you'll get an error, The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store.

To deal with that you can either blow away the data store created prior to this or add another version of the store:

  1. Add model version ("Notebook 2")
  2. Set Notebook 2 as the current model version
  3. Add the ticdsSyncID attribute to both entities in Notebook 2 following the sections below.
  4. Change the code that initializes the persistent store coordinator to:
...
   NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                            [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                            [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

   __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
   if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error]) {
       [[NSApplication sharedApplication] presentError:error];
       [__persistentStoreCoordinator release], __persistentStoreCoordinator = nil;
       return nil;
   }
Change the Managed Object Context's Concurrency Type

Open the iOSNotebookAppDelegate.m and scroll down to the managedObjectContext method declaration. Modify the __managedObjectContext initialization like so:

...    
    __managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [__managedObjectContext setPersistentStoreCoordinator:coordinator];

    return __managedObjectContext;
}
Add the Sync Attribute to the Entities in the Data Model:

Open Notebook.xcdatamodeld and select the Note entity. Add a new String attribute called ticdsSyncID, and mark it as indexed:

Modifying the data model

Do the same for the Tag entity.

Change the Managed Object Subclass:

Both the Note and Tag entities are set to use custom subclasses rather than be plain NSManagedObjects. Open the TINBNote.h file and change the @interface to inherit from TICDSSynchronizedManagedObject. You’ll need to import the TICoreDataSync.h file:

#import "TICoreDataSync.h"

@class TINBNote;

@interface TINBNote : TICDSSynchronizedManagedObject {
...

Do the same for the TINBTag class description.

#import "TICoreDataSync.h"

@class TINBNote;

@interface TINBTag : TICDSSynchronizedManagedObject {

Adding Dropbox Support

Before you can do anything else with TICoreDataSync, your application will need to register an instance of an application sync manager. For this iOS tutorial, you’ll be using the DropboxSDK-based sync classes, which means that before you can register a sync manager, you’ll need to add support into the project for the DropboxSDK.

Configuring Dropbox Access via the DropboxSDK

The DropboxSDK allows access to your Dropbox files via a rest client. The TICoreDataSync classes expect you to have set up a DBSession before calling any sync manager methods, so you’ll need to add code to the iOSNotebookAppDelegate.m file to configure a session, check if it has the necessary credentials, and if not, display the login navigator provided by the DropboxSDK.

Set the App Delegate to Conform to the Necessary Protocols

Import the DropboxSDK.h file at the top of iOSNotebookAppDelegate.m, and modify the class extension declaration at the top of the file to indicate that it adopts the DBSessionDelegate protocol:

#import "DropboxSDK.h"

@interface iOSNotebookAppDelegate () <DBSessionDelegate>

@end
Define Your Dropbox Developer Key and Secret

You’ll need your own Dropbox key and secret specific to your application before you can make connections with the DropboxSDK. For the purposes of this tutorial, you’ll need to register a “Development” application via https://www.dropbox.com/developers/apps. Once you have your secret and key for the app, add the following #defines at the top of iOSNotebookAppDelegate.m:

#define kTICDDropboxSyncKey @"yourKey"
#define kTICDDropboxSyncSecret @"yourSecret"
Configure the Dropbox Session and Request a Login if Required

Find the application:didFinishLaunchingWithOptions: method. You’ll need to start by creating a DBSession, and setting it as the default session:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
    DBSession *session = [[DBSession alloc] initWithAppKey:kTICDDropboxSyncKey appSecret:kTICDDropboxSyncSecret root:kDBRootDropbox];
    session.delegate = self;
    [DBSession setSharedSession:session];
    [session release];

The DropboxSDK stores the login information once the user has logged in successfully. A DBSession object will respond YES to isLinked if it has the necessary credentials; if so, you’ll be able to continue by registering an Application Sync Manager. If this is the first time the user has launched the app, you’ll need to request a login:

...

    if ([[DBSession sharedSession] isLinked]) {
        [self registerSyncManager];
    } else {
        [[DBSession sharedSession] linkFromController:self.navigationController];
    }

    return YES;
}

You’ll write the registerSyncManager method in the next main section.

Implement the Session Delegate Method

The DropboxSDK delegate methods you need to implement will be called when the user logs in, cancels logging in, or if there is an authorization failure (because the stored Dropbox credentials are incorrect). Start by implementing the DBSessionDelegate authorization challenge method to display the DropboxSDK login controller:

#pragma mark - DBSessionDelegate methods

- (void)sessionDidReceiveAuthorizationFailure:(DBSession *)session userId:(NSString *)userId
{
    [[DBSession sharedSession] linkFromController:self.navigationController];
}

When the user enters their credentials and logs in successfully the DropboxSDK (or the Dropbox app if installed) will launch your app with the URL scheme db-yourKey. As such you will need to register for this URL scheme in the iOSNotebook-Info.plist file. Right-click on the iOSNotebook-Info.plist file in the Project Navigator and choose Open As > Source Code and add the following key above the tag:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>db-yourKey</string>
        </array>
    </dict>
</array>

Implement the application:handleOpenURL: method thusly:

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url
{
    if ([[DBSession sharedSession] handleOpenURL:url]) {
        if ([[DBSession sharedSession] isLinked]) {
            NSLog(@"App linked successfully!");
            [self registerSyncManager];
        }

        return YES;
    }

    return NO;
}

That’s it for the DropboxSDK implementation; next you’ll need to deal with the Application Sync Manager.

The Application Sync Manager

The TICDSApplicationSyncManager is responsible for creating the initial remote file hierarchy for your application, if necessary, as well as the hierarchy specific to each registered client device. Its delegate callbacks allow you to configure how synchronization works, including specifying whether to use encryption.

You’ll need to implement the registerSyncManager method you referenced above, as well as a few required delegate callbacks:

Adopt the Application Sync Manager Delegate Protocol

Change the @interface in iOSNotebookAppDelegate.h to indicate that the class adopts the TICDSApplicationSyncManagerDelegate protocol. You’ll need to import the TICoreDataSync.h file:

#import "TICoreDataSync.h"

@interface iOSNotebookAppDelegate () <..., TICDSApplicationSyncManagerDelegate>

@end

The protocol includes a few required methods, which you’ll implement later.

Fetch the Default Manager and Register it:

Request the defaultApplicationSyncManager via the correct sync manager type. For the DropboxSDK under iOS, this is the DropboxSDK-Based manager. Start by implementing the registerSyncManager method like this:

#pragma mark - Local methods

- (void)registerSyncManager
{
    TICDSDropboxSDKBasedApplicationSyncManager *manager = [TICDSDropboxSDKBasedApplicationSyncManager defaultApplicationSyncManager];

Get the unique sync identifier for this client, and generate one if it doesn’t already exist:

    NSString *clientUuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"iOSNotebookAppSyncClientUUID"];
    if (clientUuid == nil) {
        clientUuid = [TICDSUtilities uuidString];
        [[NSUserDefaults standardUserDefaults] setValue:clientUuid forKey:@"iOSNotebookAppSyncClientUUID"];
    }

Use the UIDevice class to find out the name of the current device running the application. This will be used as the device description (human readable information to help a user distinguish between multiple registered devices):

    NSString *deviceDescription = [[UIDevice currentDevice] name];

If, for some reason, you need to use a specific DBSession for sync other than the defaultSession, you would need to specify it as the manager’s dbSession property here. By default, the sync managers will use the defaultSession, which is fine for this tutorial. Finally, register the sync manager and provide the information:

    [manager registerWithDelegate:self
              globalAppIdentifier:@"com.yourcompany.notebook"
           uniqueClientIdentifier:clientUuid
                      description:deviceDescription
                         userInfo:nil];
}

Note that the globalAppIdentifier parameter must be the same for every client, whether iOS or Mac.

Implement the Required Delegate Methods:

The TICDSApplicationSyncManagerDelegate protocol includes three required methods; if you don’t implement these, you’ll get compiler warnings when you build the project. The first required method will be called the very first time the app is registered by any client, to determine whether to use encryption. Once this delegate method is called, the application registration process is paused so you can present UI to ask the user. If you’ve already followed the Mac version of this tutorial and have enabled encryption, you’ll need to specify the password here. Otherwise, continue registration without using encryption:

#pragma mark - TICDSApplicationSyncManagerDelegate methods

- (void)applicationSyncManagerDidPauseRegistrationToAskWhetherToUseEncryptionForFirstTimeRegistration:(TICDSApplicationSyncManager *)aSyncManager
{
    [aSyncManager continueRegisteringWithEncryptionPassword:nil];
}
- (void)applicationSyncManagerDidPauseRegistrationToRequestPasswordForEncryptedApplicationSyncData:(TICDSApplicationSyncManager *)aSyncManager
{
    [aSyncManager continueRegisteringWithEncryptionPassword:nil];
}

The third required method will be called when an existing, previously synchronized document is downloaded to a client. In a document-based application, you’d use this method to return a configured Document Sync Manager for that downloaded document, but since this is a non-document-based app, just return nil as this method won’t be called:

- (TICDSDocumentSyncManager *)applicationSyncManager:(TICDSApplicationSyncManager *)aSyncManager preConfiguredDocumentSyncManagerForDownloadedDocumentWithIdentifier:(NSString *)anIdentifier atURL:(NSURL *)aFileURL
{
    return nil;
}

Look at the ShoppingList example application to see how this method should be implemented in a document-based app. (Editor's note: This example app no longer exists but I need to resurrect it. It's in the git history.)

Once the application sync manager is registered, you’ll need to configure and register the document sync manager, responsible for synchronizing the application’s data.

The Document Sync Manager

The TICDSDocumentSyncManager is responsible for creating the remote hierarchy specific to a document, downloading and uploading the entire store, performing a sync, and cleaning up unneeded files.

In a document-based application, you have one document sync manager per document. Although the iOSNotebook application is a non-document-based application, you’ll need to think of it as being a document-based application that only ever has one document.

Typically, a document-based application would keep track of a unique document synchronization identifier for each document; the Shopping List application, for example, saves this identifier in the metadata of a document’s persistent store.

For a non-document-based application, this identifier can be hard-wired into the application. When the application sync manager has completed its registration, the document sync manager can fire up its registration.

Adopt the Document Sync Manager Delegate Protocol:

Start by changing the @interface in iOSNotebookAppDelegate.h by adding yet another delegate protocol, TICDSDocumentSyncManagerDelegate:

@interface iOSNotebookAppDelegate : NSObject <... , TICDSDocumentSyncManagerDelegate> {
    ...
Keep Track of the Document Sync Manager:

Add a property declaration to the app delegate to keep track of the document sync manager:

@interface iOSNotebookAppDelegate : NSObject <...> {
    ...
}
...
@property (retain) TICDSDocumentSyncManager *documentSyncManager;
@end

You need to keep a reference to the document sync manager so that you can initiate future tasks like synchronization.

Register the Document Sync Manager:

Implement applicationSyncManagerDidFinishRegistering: to mark our managedObjectContext as synchronized and to trigger the creation of the document sync manager when the application sync manager has registered:

- (void)applicationSyncManagerDidFinishRegistering:(TICDSApplicationSyncManager *)aSyncManager
{
    self.managedObjectContext.synchronized = YES;

    TICDSDropboxSDKBasedDocumentSyncManager *docSyncManager = [[TICDSDropboxSDKBasedDocumentSyncManager alloc] init];

Again, if you need to use a different DBSession from the defaultSession, you should specify the docSyncManager’s dbSession here; the default behavior is fine for this tutorial, as before.

Register it, using a hard-wired document identifier:

    [docSyncManager registerWithDelegate:self 
                          appSyncManager:aSyncManager 
                    managedObjectContext:[self managedObjectContext]
                      documentIdentifier:@"Notebook" 
                             description:@"Application's data" 
                                userInfo:nil];

Finally, set the property (which will retain it) and release it to balance the alloc] init]:

    [self setDocumentSyncManager:docSyncManager];
    [docSyncManager release];
}
Implement the Required Delegate Methods:

There are four required document sync manager delegate methods. One is called if a conflict is found during the synchronization process. In a shipping app, you would probably want to ask the user how to proceed, but for this tutorial, just implement the method to continue synchronizing with the local change taking precedent:

#pragma mark - TICDSDocumentSyncManagerDelegate methods

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didPauseSynchronizationAwaitingResolutionOfSyncConflict:(id)aConflict
{
    [aSyncManager continueSynchronizationByResolvingConflictWithResolutionType:TICDSSyncConflictResolutionTypeLocalWins];
}

Another is called to find out the location on disk of the store file to be uploaded:

- (NSURL *)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager URLForWholeStoreToUploadForDocumentWithIdentifier:(NSString *)anIdentifier description:(NSString *)aDescription userInfo:(NSDictionary *)userInfo
{
    return [[self applicationFilesDirectory] URLByAppendingPathComponent:@"Notebook.storedata"];
}

The delegate will be notified if syncing fails. For now just log the error:

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didFailToSynchronizeWithError:(NSError *)anError
{
    NSLog(@"%s %@", __PRETTY_FUNCTION__, anError);
}

The final required delegate methods are called if the remote file structure doesn’t exist for the document at the time of registration, or if the document has previously been deleted. In a shipping application, you might want to ask the user what to do, at least if the document was deleted. For now, just implement both to tell the document sync manager to continue registration:

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didPauseRegistrationAsRemoteFileStructureDoesNotExistForDocumentWithIdentifier:(NSString *)anIdentifier description:(NSString *)aDescription userInfo:(NSDictionary *)userInfo
{
    [aSyncManager continueRegistrationByCreatingRemoteFileStructure:YES];
}

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didPauseRegistrationAsRemoteFileStructureWasDeletedForDocumentWithIdentifier:(NSString *)anIdentifier description:(NSString *)aDescription userInfo:(NSDictionary *)userInfo
{
    [aSyncManager continueRegistrationByCreatingRemoteFileStructure:YES];
}

Don’t run the app yet, as you need to determine what happens the first time a client tries to register.

Store Upload and Download

When a client registers, it should check whether it has existing data of its own. If not, it needs to download the most recent store that’s been uploaded by other registered clients, assuming such a store exists.

Keep Track of Whether to Download the Store:

Start by adding a BOOL instance variable and property:

@interface iOSNotebookAppDelegate : NSObject <...> {
    ...
}
...
@property (nonatomic, assign, getter = shouldDownloadStoreAfterRegistering) BOOL downloadStoreAfterRegistering;

@end
Decide Whether to Download the Store:

You’ll need to add a check for existing data before the Core Data stack is set up. The easiest place to do this is just before the persistent store coordinator is created:

- (NSPersistentStoreCoordinator *) persistentStoreCoordinator
{
    ...
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Notebook.sqlite"];
    
    /* Add the check for an existing store here... */
    if ([[NSFileManager defaultManager] fileExistsAtPath:storeURL.path] == NO) {
        self.downloadStoreAfterRegistering = YES;
    }

    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    ...
}
Download the Store After Registering:

If the store needs to be downloaded, this should be done just after the document sync manager finishes registering. If the store does not need to be downloaded then just kick off a synchronization.

- (void)documentSyncManagerDidFinishRegistering:(TICDSDocumentSyncManager *)aSyncManager
{
    if (self.shouldDownloadStoreAfterRegistering) {
        [aSyncManager initiateDownloadOfWholeStore];
    } else {
        [aSyncManager initiateSynchronization];
    }
}
Don’t Download at First Launch:

If this is the very first time the app has been registered by any device, you won’t be able to download the store because no previous stores will exist.

As you saw earlier, one of the required delegate methods will be called by the document sync manager to find out what to do if no remote file structure exists for a document, or if the document has been deleted. Change your implementation of these methods to prevent the store download:

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didPauseRegistrationAsRemoteFileStructureDoesNotExistForDocumentWithIdentifier:(NSString *)anIdentifier description:(NSString *)aDescription userInfo:(NSDictionary *)userInfo
{
    self.downloadStoreAfterRegistering = NO;
    [aSyncManager continueRegistrationByCreatingRemoteFileStructure:YES];
}

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didPauseRegistrationAsRemoteFileStructureWasDeletedForDocumentWithIdentifier:(NSString *)anIdentifier description:(NSString *)aDescription userInfo:(NSDictionary *)userInfo
{
    self.downloadStoreAfterRegistering = NO;
    [aSyncManager continueRegistrationByCreatingRemoteFileStructure:YES];
}
Do Download if Client was Deleted:

If another client has previously deleted this client from synchronizing with the document, the underlying helper files will automatically be removed, but you will need to initiate a store download to override the whole store document file you have locally (as it will be out of date compared to the available sets of sync changes).

In a shipping application, you may want to copy the old store elsewhere in case the user wishes to restore it. For now, just implement the client deletion delegate warning method to indicate that the store should be downloaded.

Note that the registration process cannot be stopped at this point, so you do not need to call any continueRegistration method:

- (void)documentSyncManagerDidDetermineThatClientHadPreviouslyBeenDeletedFromSynchronizingWithDocument:(TICDSDocumentSyncManager *)aSyncManager
{
    self.downloadStoreAfterRegistering = YES;
}
Upload an Existing Store at Document Registration:

In order for other clients to be able to download the whole store, one client will obviously need to upload a copy of the store at some point.

The document sync manager will ask whether to upload the store during document registration. Implement this method to return YES, but only if this isn’t the first time this client has been registered:

- (BOOL)documentSyncManagerShouldUploadWholeStoreAfterDocumentRegistration:(TICDSDocumentSyncManager *)aSyncManager
{
    return self.shouldDownloadStoreAfterRegistering == NO;
}

In a shipping iOS application, you might wish to ask the user whether they want to upload the store every time they run the app. If the user is synchronizing with a Mac client, they may prefer that only the Mac client uploads the store to save iOS bandwidth and time. But, if they are synchronizing iOS to iOS without any Mac clients, one of those iOS clients will need to upload the store every so often.

Handle Replacement of the Persistent Store File:

If the store file is downloaded, it will replace any file that has been created on disk. You’ll need to implement two delegate methods to make sure the persistent store coordinator can cope with the file being removed.

First, implement the method called just before the store is replaced:

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager willReplaceStoreWithDownloadedStoreAtURL:(NSURL *)aStoreURL
{
    NSError *anyError = nil;
    BOOL success = [self.persistentStoreCoordinator removePersistentStore:[self.persistentStoreCoordinator persistentStoreForURL:aStoreURL] error:&anyError];

    if (success == NO) {
        NSLog(@"Failed to remove persistent store at %@: %@", aStoreURL, anyError);
    }
}

Second, the method called just after the store is replaced:

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didReplaceStoreWithDownloadedStoreAtURL:(NSURL *)aStoreURL
{
    NSError *anyError = nil;
    id store = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:aStoreURL options:nil error:&anyError];

    if (store == nil) {
        NSLog(@"Failed to add persistent store at %@: %@", aStoreURL, anyError);
    }
}

Updating the View Controller’s Content if the Store Changes

The Mac version of this tutorial uses bindings to keep the interface updated when the underlying data is changed. In this iOS application, you’ll need to make changes to each of the primary view controllers to adjust the interface items.

Update the Root View Controller:

The RootViewController displays a standard table view of notes, and uses a fetched results controller. When the persistent stores change, the FRC will need to perform a new fetch to update the table view. Start by registering to receive notifications when the persistent store changes at the end of the viewDidLoad method in RootViewController.m:

- (void)viewDidLoad
{
    [super viewDidLoad];
    ...

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(persistentStoresDidChange:) name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];
}

Unregister for this notification in viewDidUnload:

- (void)viewDidUnload
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];

    [super viewDidUnload];
}

Implement the persistentStoresDidChange: method like this:

#pragma mark - NSPersistentStoreCoordinatorStoresDidChangeNotification method

- (void)persistentStoresDidChange:(NSNotification *)aNotification
{
    NSError *anyError = nil;
    BOOL success = [self.fetchedResultsController performFetch:&anyError];
    if (success == NO) {
        NSLog(@"Error fetching: %@", anyError);
    }

    [self.tableView reloadData];
}
Update the Note Tags View Controller

The NoteTagsViewController also displays a standard table view, but shows the list of available tags, with checkmarks against those applied to a note. It also uses a fetched results controller. Make the same implementation changes as you did above for the RootViewController. You’ll need to access the managedObjectContext via: [[self note] managedObjectContext]

Update the Note View Controller

The NoteViewController displays a selected note, including its title, content and tags. Different controls are displayed depending on whether or not the user is editing a note. Add similar methods to register for a notification when the persistent stores change:

- (void)viewDidLoad
{
    [super viewDidLoad];
    ...

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(persistentStoresDidChange:) name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.note.managedObjectContext.persistentStoreCoordinator];
}

And unregister:

- (void)viewDidUnload
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.note.managedObjectContext.persistentStoreCoordinator];

    [super viewDidUnload];
}

Each of the controls needs to be updated, but only if the user is not currently editing it:

#pragma mark - NSPersistentStoreCoordinatorStoresDidChangeNotification method

- (void)persistentStoresDidChange:(NSNotification *)aNotification
{
    [self.note.managedObjectContext refreshObject:self.note mergeChanges:YES];

    if (self.editingTextView.isFirstResponder == NO) {
        self.editingTextView.text = self.note.content;
    }

    if ( self.titleTextField.isFirstResponder == NO) {
        self.titleTextField.text = self.note.title;
    }

    [self updateTags];
}
Enable Logging

TICoreDataSync can be extremely verbose when it comes to logging. Thankfully we've also provided logging levels so you can tune how much logging you wish to see from the framework. Set the log verbosity to TICDSLogVerbosityEveryStep so you can bask in the glory of the wall of text that TICDS will crit you with the next time it runs. Do this in the iOSNotebookAppDelegate's +initialize method:

+ (void)initialize
{
    [TICDSLog setVerbosity:TICDSLogVerbosityEveryStep];
}
Testing the Application

At this point, you’re ready to run the application to test store upload and download behavior, if you wish.

The first time you run the app on any device, you’ll find a directory is created at the root of your Dropbox, called com.yourcompany.notebook. This contains all the remote files used by TICoreDataSync to synchronize clients’ data. The file structure is described further in the Remote File Hierarchy document.

What’s missing at this point, however, is the main reason for using TICoreDataSync—the ability to synchronize changes made after the initial store upload/download.

Initiating Synchronization

The first thing to add is a suitable UI element to initiate synchronization.

Add an Action to the App Delegate:

Open iOSNotebookAppDelegate.h and add the signature for an IBAction method:

@interface iOSNotebookAppDelegate : NSObject <...> {
     ...
 }
 ...
 - (IBAction)beginSynchronizing:(id)sender;
 @end

Implement the method in iOSNotebookAppDelegate.m, like this:

- (IBAction)beginSynchronizing:(id)sender
{
    // Save the managed object context to cause sync change objects to be written
    NSError *saveError = nil;
    [self.managedObjectContext save:&saveError];
    if (saveError != nil) {
        NSLog(@"%s %@", __PRETTY_FUNCTION__, saveError);
    }

    [self.documentSyncManager initiateSynchronization];
}
Add a Synchronize Button in the User Interface:

The easiest place to add a synchronize button is in the navigation bar of the root view controller. Open RootViewController.m, and set the left button to be a refresh button, set to target the beginSynchronizing: method on the app delegate:

- (void)viewDidLoad
{
    ...
    UIBarButtonItem *syncButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:[[UIApplication sharedApplication] delegate] action:@selector(beginSynchronizing:)];
    self.navigationItem.leftBarButtonItem = syncButton;
    [syncButton release];
    ...
}
Save Changes Made During Synchronization:

When TICoreDataSync applies changes made by other clients, it does so in a background managed object context that is a child of the primary context (the one you supplied when you registered the document sync manager). You’ll need to implement another delegate method in iOSNotebookAppDelegate.m to alert you when changes are made and saved from the background context, so that you can save your main context. This step isn't necessary but it does work around an existing known issue.

- (void)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager didMakeChangesToObjectsInBackgroundContextAndSaveWithNotification:(NSNotification *)aNotification
{
    NSError *saveError = nil;
    [self.managedObjectContext save:&saveError];
    if (saveError != nil) {
        NSLog(@"%s %@", __PRETTY_FUNCTION__, saveError);
    }
}
Testing the Application

Build and run the application, add some notes and tags, then tap the sync button. When you sync iOSNotebook it initiates a save and TICoreDataSync jumps into action to create Sync Change objects to describe what’s been changed. These are stored in a separate, private managed object context.

During synchronization, any changes made by other clients are pulled down first. Any conflicts are fixed with the local, unpushed sync changes, then the local changes are pushed to the remote.

If you built the Mac tutorial, or test the iOS app on one or more devices as well as the simulator, make sure each client pulls down the changes correctly.

Making Synchronization Appear Automatic

There are two other features you can implement to make synchronization appear more seamless.

Firstly, TICoreDataSync will ask whether it should initiate a synchronization whenever it detects that the primary context has been saved. If you respond with YES, synchronization will occur every time the user saves their data.

Secondly, the Document Sync Manager offers the ability to detect when other clients have pushed sync changes, at which point it will initiate a synchronization. In a sync environment of multiple devices this means that when one client saves, the changes will be pulled down by all the other synchronized clients in very short fashion.

Initiate Synchronization After Every Context Save:

Implement this document sync manager delegate method to return YES:

- (BOOL)documentSyncManager:(TICDSDocumentSyncManager *)aSyncManager shouldBeginSynchronizingAfterManagedObjectContextDidSave:(NSManagedObjectContext *)aMoc;
{
    return YES;
}
Enable Automatic Synchronization After Changes are Detected:

You’ll need to turn on remote change polling immediately after the document sync manager has finished registering, so add the following into the relevant delegate method:

- (void)documentSyncManagerDidFinishRegistering:(TICDSDocumentSyncManager *)aSyncManager
{
...
    [aSyncManager beginPollingRemoteStorageForChanges];
}

If your app is using the Dropbox API (as this iOS example app is) you'll notice that it takes longer for it to detect changes made by other clients. This is because the Dropbox folder is only polled once per minute for the DropboxSDK-based classes whereas the File Manager-based classes use a directory watcher implementation.

Testing the Application

Test the application once more, if possible on multiple devices, to check that these features work as expected.

Any changes made by any other client will automagically be propagated to other on synchronized clients.

Displaying Progress Indicators

It would be nice if the interface could display an animated progress indicator whenever synchronization tasks were taking place.

TICoreDataSync offers two ways to implement progress indication. For task-specific progress, you could implement every didBegin, didFinish, and didFailTo delegate method, and display suitable progress updates. Alternatively, both application and document sync managers post notifications when they start and end a task.

Let’s take the easy approach, and use these notifications.

Using the Notifications

You’ll need to register for four notifications—two posted by the application sync manager, two by the document sync manager. These are intended to be used as indications when activity increases and decreases.

For the iOSNotebook app, it seems best to indicate both application and document activity by using the status bar activity indicator.

Keep Track of the Activity Count

Start by adding an NSInteger property to keep track of the activity count:

 @interface iOSNotebookAppDelegate : NSObject <...> 
   ...
 
@property (nonatomic, assign) NSInteger activity;

  @end

The status bar activity indicator needs to be hidden/shown according to the activity count.

Register for Activity Notifications:

Register for the application and document sync manager notifications just after the application finishes launching. In a shipping application you would want to register for notifications from the actual sync managers (especially in a document-based environment), but in this case we're just passing nil to the object: argument.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(activityDidIncrease:) name:TICDSApplicationSyncManagerDidIncreaseActivityNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(activityDidDecrease:) name:TICDSApplicationSyncManagerDidDecreaseActivityNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(activityDidIncrease:) name:TICDSDocumentSyncManagerDidIncreaseActivityNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(activityDidDecrease:) name:TICDSDocumentSyncManagerDidDecreaseActivityNotification object:nil];

...
Implement the Activity Methods:

Add the following methods that will be called when notifications are posted:

#pragma mark - Sync Manager Activity Notification methods

- (void)activityDidIncrease:(NSNotification *)aNotification
{
    self.activity++;

    if (self.activity > 0) {
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    }
}

- (void)activityDidDecrease:(NSNotification *)aNotification
{
    if (self.activity > 0) {
        self.activity--;
    }

    if (self.activity < 1) {
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
    }
}
Testing the Application

Test the application once again. The activity indicator should appear whenever activity is occurring.

Adding Encryption

TICoreDataSync can encrypt all important synchronization data before it is transferred to the remote. For the iOSNotebook application, this means that all synchronization files will be encrypted before they are uploaded to Dropbox by the REST client.

If you’ve previously followed the Mac tutorial, and enabled encryption, you can skip this section.

Implementing the Encryption Methods

Encryption can only be enabled the first time any client registers to synchronize an application’s data. This means you’ll need to remove any existing remote sync data before continuing, either manually, or by asking the application sync manager to remove all data (not yet implemented in the framework).

Remove Existing Data:

If you’ve already launched the application and tested it by synchronizing data, you’ll need to quit the Notebook/iOSNotebook application on all clients, then delete the entire directory at ~/Dropbox/com.yourcompany.notebook from a desktop Mac, or via the Dropbox web interface.

Re-Implement the Initial Registration Encryption Delegate Method:

You only need to modify two delegate methods to inform TICoreDataSync that it should encrypt all important data. The first method is called the first time any client registers to synchronize data for an application:

- (void)applicationSyncManagerDidPauseRegistrationToAskWhetherToUseEncryptionForFirstTimeRegistration:(TICDSApplicationSyncManager *)aSyncManager
{

The existing implementation of this method continues registration by passing nil as the password, meaning that the data won’t be encrypted. In a shipping application, you’d obviously want to display suitable UI to ask the user whether they want their data encrypted, and if so what password to use, but for this tutorial, just hard-wire a password. Change the implementation of this method to specify a password:

    [aSyncManager continueRegisteringWithEncryptionPassword:@"password"];
}
Re-Implement the Initial Client Registration Method:

The above method takes care of the first time an application is registered. For additional clients registering against existing data, the framework will detect if encryption is enabled, and request a password if necessary. Again, in a shipping application you’d need to display suitable UI to ask the user for the password (if they supply an incorrect password, this method will be called repeatedly), but for this tutorial, just hard-wire the same password. Change the implementation of the other encryption method to specify the password:

- (void)applicationSyncManagerDidPauseRegistrationToRequestPasswordForEncryptedApplicationSyncData:(TICDSApplicationSyncManager *)aSyncManager
{
    [aSyncManager continueRegisteringWithEncryptionPassword:@"password"];
}
Testing the Application

Once again, test that the application behaves as expected. Nothing will appear to have changed from the user experience point of view, but if you try to open any of the files on the remote (i.e., in ~/Dropbox/com.yourcompany.notebook from a desktop Mac) such as a deviceInfo.plist file, you’ll find the content appears garbled and unreadable in a text editor.

⚠️ **GitHub.com Fallback** ⚠️