Using iCloud Key Value Store in Titanium

iCloud is a surprisingly useful tool for app developers but it has extremely limited support in Titanium Mobile.

Apple's iCloud Design Guide states that "Every app submitted to the App Store or Mac App Store should take advantage of key-value storage" and yet support for iCloud Key Value Storage is not built in to Titanium Mobile.

If you want your users to have the same experience on all of their mePhones and mePads or be able to migrate their app seamlessly from an old phone to a new phone, iCloud is a great easy way to do it. For my upcoming Sleep App I implemented in app purchasing with in app subscriptions. Apple requires that users are able to restore their purchase history on any device they own. Since there is no way to use the app store to get this information for subscriptions iCloud is a great way to go.

In a nutshell, the iCloud Key Value store is a data store for small amounts of data that will be shared among all the meDevices that a user owns. So, if you save a setting on DeviceA, that setting is available on DeviceB. Of course network connectivity is required to synchronise the values, but we can store and retrieve values offline too.

Create a new empty Titanium iOS Module

The full instructions for setting up your envirornment for iOS module development and getting started can be found here. If you have already set up your environment and know it is working you should start by switching to the directory where you want to create a new project directory and executing the following command.

titanium.py create --platform=iphone --type=module --dir=./ --name=iCloudKeyValue --id=com.example.iCloudKeyValue

Change to the newly created directory and build and run the project

Execute the build and run commands as shown to test if your iOS sample module is working.
The IOS Simulator should start up and display a Hello World message or something similar

./build.py
titanium.py run

Find the xCode project in the directory and open it

If you list the directory contents you will find the xcode project file. In my case it is iCloudKeyValue.xcodeproj

open iCloudKeyValue.xcodeproj

Open the module header file in xCode

In my case it is called ComExampleICloudKeyValueModule.h

Add a variable to store a reference to the iCloud Key Value Store object


/**
 * Your Copyright Here
 *
 * Appcelerator Titanium is Copyright (c) 2009-2010 by Appcelerator, Inc.
 * and licensed under the Apache Public License (version 2)
 */
#import "TiModule.h"

@interface ComExampleICloudKeyValueModule : TiModule 
{
}
 @property (strong, nonatomic) NSUbiquitousKeyValueStore *keyStore;

@end

Synthesize the keyStore property in the implementation file

In my case the implementation file is called ComExampleICloudKeyValueModule.m. Open it in the editor and add the following code


/**
 * Your Copyright Here
 *
 * Appcelerator Titanium is Copyright (c) 2009-2010 by Appcelerator, Inc.
 * and licensed under the Apache Public License (version 2)
 */
#import "ComExampleICloudKeyValueModule.h"
#import "TiBase.h"
#import "TiHost.h"
#import "TiUtils.h"

@implementation ComExampleICloudKeyValueModule
@synthesize keyStore;

#pragma mark Internal

Initialize the keyStore when the module is loaded


// this is generated for your module, please do not change it
-(NSString*)moduleId
{
	return @"com.example.iCloudKeyValue";
}

#pragma mark Lifecycle

-(void)startup
{
	// this method is called when the module is first loaded
	// you *must* call the superclass
	[super startup];
	
    keyStore = [[NSUbiquitousKeyValueStore alloc] init];

	
	NSLog(@"[INFO] %@ loaded",self);
}

Remove example properties and methods

Delete the lines with strikethough below.


-(void)_listenerRemoved:(NSString *)type count:(int)count
{
	if (count == 0 && [type isEqualToString:@"my_event"])
	{
		// the last listener called for event named 'my_event' has
		// been removed, we can optionally clean up any resources
		// since no body is listening at this point for that event
	}
}

#pragma Public APIs


 
 -(id)example:(id)args
 {
 	// example method
 	return @"hello world";
 }

 -(id)exampleProp
 {
 	// example property getter
 	return @"hello world";
 }

 -(void)setExampleProp:(id)value
 {
 	// example property setter
 }


@end

Add a getString method for returning a string from iCloud

Add the whole method as shown. The real code is
NSString *storedString = [keyStore stringForKey:keyString];.
The other lines in the method are just verifying the arguments are of the correct type and extracting the key value from the arguments.


#pragma Public APIs

-(id)getString:(id)args
{
    ENSURE_SINGLE_ARG(args, NSDictionary);
    NSString *keyString = [TiUtils stringValue:@"key" properties:args def:@""];
    NSString *storedString = [keyStore stringForKey:keyString];
   	return storedString;
}

@end

Add a setString method for saving a string to iCloud

Again the main work is done by the highlighted lines and the rest is just parameter validation. It is important to note that if you have multiple clients updating values simultaneously or during periods of intermittent connectivity you might need to deal with potential conflicts when running the synchronize method. In this simple scenario we will ignore these issues and assume that the value we are saving is the most up to date.


#pragma Public APIs

-(id)getString:(id)args
{
    ENSURE_SINGLE_ARG(args, NSDictionary);
    NSString *keyString = [TiUtils stringValue:@"key" properties:args def:@""];
    NSString *storedString = [keyStore stringForKey:keyString];
   	return storedString;
}

-(void)setString:(id)args
{
    ENSURE_SINGLE_ARG(args, NSDictionary);
    NSString *keyString = [TiUtils stringValue:@"key" properties:args def:@""];
    NSString *valueString = [TiUtils stringValue:@"value" properties:args def:@""];
    [keyStore setString:valueString forKey:keyString];
    [keyStore synchronize];
}

@end

Add a getAllValues and removeObject methods

Add these methods after your other methods but before the @end directive



-(id)getAllValues:(id)args
{
    [keyStore synchronize];
    return [keyStore dictionaryRepresentation];
}

-(void)removeObject:(id)args
{
    ENSURE_SINGLE_ARG(args, NSDictionary);
    NSString *keyString = [TiUtils stringValue:@"key" properties:args def:@""];
    [keyStore removeObjectForKey: keyString];
}

@end

Build the project

Use the build.py command as before in the project directory.
At this point you might expect that we would amend the sample app to work with our new module and run it. There are two problems with that:

  1. iCloud only runs on actual iOS devices
  2. iCloud requires a special provisioning profile that specifically grants iCloud access
    So, instead we are going to install the plugin and create a seperate Titanium project for testing.

./build.py

Install the module

On Mountain Lion this is the correct directory you may find that it is /Library/Application\ Support/Titanium on previous versions


cp com.example.icloudkeyvalue-iphone-0.1.zip ~/Library/Application\ Support/Titanium/

Create a new Titanium Mobile project and add a reference to the new plugin

Add the following code to tiapp.xml

    <modules>
        <module version="0.1">com.example.icloudkeyvalue</module>
    </modules>

Replace code in app.js with some testing code

A simple test app that saves and retreives a value from the store


// open a single window
var win = Ti.UI.createWindow({
	backgroundColor:'white'
});
var verticalView = Ti.UI.createView({
	layout:"vertical"
});
win.add(verticalView);


var savedText = Ti.UI.createTextField({
	width: 250, 
	height: 60,
	borderStyle: 1
});
verticalView.add(savedText);

var saveButton = Ti.UI.createButton({
	title: "Save Setting"
})
verticalView.add(saveButton);

win.open();


var iCloudModule = require('com.example.iCloudKeyValue');
Ti.API.info("module is => " + iCloudModule);

savedText.value = iCloudModule.getString({
	key: "testKey"
});

saveButton.addEventListener('click',function(e) {
	iCloudModule.setString({
		key : "testKey",
		value: savedText.value
	});
})

Set up App for iCloud in the Apple member centre

Register the app ID of the new titanium project that you created. This can be found in the tiapp.xml file. It's not the program ID of the module that we created, it's the ID of the Titanium project that we have set up to test the module.

Enable iCloud Services for your App

Tick the iCloud box in enabled services and press continue.

Create a new Development Provisioning Profile for the App

Select the app ID you set up previously

Select the certificate for your account

This assumes you have already set up your development certificate with the developer center. If not, you can find more information here.
Continue with setting up your provisioning profile by selecting devices and downloading the profile to your machine.

Intall the Provisioning Profile using XCode Organizer

Click the import option and select the file you downloaded in the previous step

Create Entitlements.plist file in root of your Titanium Project

Place the following xml in the file Entitlements.plist in the root of your project. This file let's the device know that you want to use the iCloud Key Value Store. You will find that when you distribute your app for the appStore or adHoc distribution, you will need to change the get-task-allow option to false.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>get-task-allow</key>
	<true/>
	<key>com.apple.developer.ubiquity-kvstore-identifier</key>
	<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
	<key>keychain-access-groups</key>
	<array>
		<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
	</array>
</dict>
</plist>

Build and run your project on your IOS Device

Test your App

Test your app by entering something in the box and selecting Save Setting. You can test if it is working by installing the app on another one of your devices. The word you typed should appear on the other device. An alternative way of testing is to uninstall the app and reinstall it, the word you typed should appear.

Optionally set up iCloud for use as a Custom Backbone Sync Adapter in Titanium Alloy

This code is an adaptation of the properties sync adapter found in the Titanium Alloy source code. If you place this in your alloy project in the app/lib/alloy/sync folder in a file called icloud.js you will now have an icloud Sync Adapter that you can use with your backbone models.

var Alloy = require('alloy'),
	_ = require("alloy/underscore")._,
	iCloud = require('com.example.iCloudKeyValue');

function S4() {
   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}

function guid() {
   return (S4()+S4()+'-'+S4()+'-'+S4()+'-'+S4()+'-'+S4()+S4()+S4());
}

function Sync(method, model, opts) {
	var prefix = model.config.adapter.collection_name ? model.config.adapter.collection_name : 'default';
	var regex = new RegExp("^(" + prefix + ")\\-(.+)$");
	var resp = null;

	if (method === 'read') {
		if (opts.parse) {
			// is collection
			var list = [];
			var allProperties = iCloud.getAllValues();
			//Keys
			Ti.API.info("All Properties - " + JSON.stringify(allProperties));
			_.each(allProperties, function(value, key){
				var match = key.match(regex);
				if (match !== null) {
					if(value !== ""){
						list.push(JSON.parse(value));
					}
					
				}
			})
			
			resp = list;
		} else {
			// is model
			var obj = JSON.parse(iCloud.getString(prefix + '-' + model.id));
			model.set(obj);
			resp = model.toJSON();
		}
	}
	else if (method === 'create' || method === 'update') {
		if (!model.id) {
			model.id = guid();
			model.set(model.idAttribute, model.id);
		}
		iCloud.setString({
			key : prefix + '-' + model.id,
			value : JSON.stringify(model)
		});
		Ti.API.info("Saving Model " +JSON.stringify(model));
		resp = model.toJSON();
	} else if (method === 'delete') {
		iCloud.removeObject({key : prefix + '-' + model.id });
		model.clear();
		resp = model.toJSON();
	}

	// process success/error handlers, if present
	if (resp) {
        if (_.isFunction(opts.success)) { opts.success(resp); }
        if (method === "read") { model.trigger("fetch"); }
    } else {
		if (_.isFunction(opts.error)) { opts.error(resp); }
    }
}

module.exports.sync = Sync;
module.exports.beforeModelCreate = function(config) {
	// make sure we have a populated model object
	config = config || {};
	config.columns = config.columns || {};
	config.defaults = config.defaults || {};

	// give it a default id if it doesn't exist already
	if (typeof config.columns.id === 'undefined' || config.columns.id === null) {
		config.columns.id = 'String';
	}

	return config;
};