Learn how to use CloudKit JS to create a web app to access the database of a CloudKit iOS app, making your app’s data available on the web!

Update 9/21/16: Updated for Xcode 8, iOS 10 and Swift 3.

iCloud does a lot of great things. It bridges the gap between iOS and macOS by storing and syncing app data and files so the user can access their stuff from any Apple device.

Notes and Photos are excellent examples of the power of this service — if your content is on one device, it’s reliably on the rest of them within moments. It’s almost magical.

In addition to the obvious use cases, iCloud hosts apps’ public and private databases and handles user authentication.

CloudKit is the framework that affords access to iCloud, providing tons of APIs to make it easier to incorporate iCloud’s magical ways into your creations.

When Apple announced CloudKit in 2014, a lot of developers requested access to their app’s CloudKit data via a web service. Apple listened: in 2015, Apple announced CloudKit Web Services, a JSON/HTTPS interface for CloudKit.

In addition, Apple took it a step further and provided CloudKit JS, which makes it simple to create a web-based interface to access a CloudKit app’s databases. CloudKit JS wraps the REST API in concise code, so you don’t have to compose paths manually or parse JSON.

As you work through this CloudKit JS tutorial, you’ll create a web app to access the database of a CloudKit iOS app. When you’re done, it’ll provide web access to users and even make app data available to non-iOS users!

Prerequisites

You’ll need basic understanding of HTML and JavaScript for this tutorial. For a quick refresher, check out W3Schools.

This CloudKit JS tutorial also assumes you have working knowledge of CloudKit. If you don’t, we suggest starting with our CloudKit Tutorial: Getting Started.

Also nice to have, but not required:

Membership in the Apple Developer or Apple Developer Enterprise program.

Some familiarity with Knockout.js to create data bindings and Skeleton to layout the web page.

CloudKit JS

CloudKit JS features a similar design to CloudKit iOS, which makes it easier for iOS developers to create CloudKit web apps. Its main features include:

Web sign in with Apple ID — your app won’t see usernames and passwords, but you’ll give users the option to be discoverable . When a user opts-in, the app will be given their name. Other users will be able to discover them if they know their email address.

. When a user opts-in, the app will be given their name. Other users will be able to discover them if they know their email address. Full CRUD (create, read, update, and delete) access to public and private databases.

Feature parity with CloudKit iOS, including subscriptions and notifications, using completions via JavaScript promises.

Best of all, because CloudKit JS feels familiar to web developers, you can simply give them your container ID, API key and database schema and let them build the web app. ;]

CloudKit JS works on mainstream browsers, including Safari, Firebox, Chrome, Internet Explorer and Microsoft Edge.

In February 2016, Apple announced that CloudKit now supports server-to-server web service requests, enabling reading and writing to CloudKit databases from server-side processes or scripts. You’ll see why this is significant at the end of this tutorial.

Getting Started

As a relative n00b to social media, I often learn new acronyms from people I follow on Twitter.

For instance, TIL means “Today I Learned” — I felt it’s the perfect name for this sample app. It’s a simple premise: users can view and add acronym definitions to a public CloudKit database.

To run the sample CloudKit iOS app, you must be a member of the Apple Developer Program or the Apple Developer Enterprise Program. You won’t be able to run the iOS app if you’re not. :[

However, non-members can still build the web app to access my CloudKit container. You’ll just need to skip certain sections related to CloudKit setup — it’ll be clear what to skip.

If you don’t have a Mac, that’s okay! You don’t need one to build the web app.

Download and unzip TIL Starter. It contains the starter iOS app, web app, and server files.

Setting up CloudKit and the iOS App

Note: This is one of those spots where “Apple Developers” part ways with those who are not. ;] If you’re not an Apple Developer, skip to the Configuring CloudKit JS section — although you won’t play around with the sample config, you’ll see what’s needed from a CloudKit app in order to create its web app.

If you are a member, follow the steps below to set up the sample CloudKit iOS app using your own iCloud container.

Open TIL.xcodeproj. Select the TIL project in the project navigator, and then under Targets\TIL\General change the Bundle Identifier and Team to match your information. Tap Fix Issue to get Xcode to create the provisioning profile.

In Targets\TIL\Capabilities, toggle iCloud OFF then ON again, and select Key-value storage, CloudKit, and Use default container.

Note: Enabling CloudKit in your app automatically creates a default container to store the app’s databases and user records. However, it only stores your app’s public data. Private data saves directly to users’ iCloud accounts. Fortunately, iCloud automatically handles user registration and creates a unique identifier for each user. You’ll add authentication later in this tutorial.

Tap CloudKit Dashboard to open the CloudKit Dashboard in a browser (use Safari for best results). You can use the dashboard to access, set up and view records online, and manage your CloudKit app.

Sign in to the iCloud account that matches the Team you set in Targets\TIL\General. Select the TIL container from the list.

Note: If you don’t see TIL, try refreshing the page. Sometimes CloudKit’s dashboard won’t show new data, but waiting a few seconds and refreshing typically fixes the issue.

Select Schema\Record Types and press + to create a new record type. Name it Acronym and add two fields. Name the first short and the second long. Make sure both fields’ type is set to String. Tap Save.

In Public Data\Default Zone, create two new Acronym records. For the first, set its long value to Today I Learned and short to TIL. For the second, set long to For The Win and short to FTW.

Note: As with the TIL container, you may not see Acronym appear under Default Zone straight away. You might just see NewRecordType until you refresh the page.

Go to the iPhone simulator, open Settings and sign in to iCloud. Build and run the TIL iOS app. Your new records should appear!

In the iOS app, tap the + button and add two items: BTW/By The Way and TBH/To Be Honest.

Back in your browser, change the Sort by order in the CloudKit dashboard’s Default Zone to sort by new items — it’s a little hack that “refreshes” the records in the CloudKit dashboard.

A Word About Knockout and Skeleton

Your web app will update its UI when users sign in or out and when the database changes by using knockout.js to create dynamic data bindings between the UI in index.html and the data model in TIL.js.

If you’re familiar with the MVVM design pattern, then know that the CloudKit database is the model, index.html is the view, and TIL.js is the view model.

Knockout provides data bindings for text, appearance, control flow and forms. You’ll inform Knockout of which view model properties can change by declaring them as observable.

You’ll also use the Skeleton responsive CSS framework to create a grid-based web UI. Each row contains 12 columns to use to display elements.

Configuring CloudKit JS

Inside the TIL Starter/Web folder are TIL.js and index.html. You’ll add JavaScript code to TIL.js to fetch and save Acronym records, and you’ll update index.html to display and get user input for Acronym records.

Open TIL.js and index.html in Xcode and go to Xcode\Preferences. In Text Editing\Indentation, uncheck Syntax-aware indenting: Automatically indent based on syntax. Xcode’s auto-indent doesn’t work well with JavaScript, but its Editor\Structure functions are still useful.

Take a look at the code in TIL.js:

// 1 window.addEventListener('cloudkitloaded', function() { console.log("listening for cloudkitloaded"); // 2 CloudKit.configure({ containers: [{ // 3 containerIdentifier: 'iCloud.com.raywenderlich.TIL', apiToken: '1866a866aac5ce2fa732faf02fec27691027a3662d3af2a1456d8ccabe9058da', environment: 'development' }] }); console.log("cloudkitloaded"); // 4 function TILViewModel() { var self = this; console.log("get default container"); var container = CloudKit.getDefaultContainer(); } // 5 ko.applyBindings(new TILViewModel()); });

window is the browser window. Apple recommends loading cloudkit.js asynchronously, so window.addEventListener('cloudkitloaded', function() { ... } attaches the function as the event handler of the cloudkitloaded event. The rest of the code in TIL.js is the cloudkitloaded event handler. After cloudkit.js loads, you configure the CloudKit containers by specifying each one’s identifier, API token and environment. Change the containerIdentifier to match the value of the default container, which should be iCloud, followed by the iOS app’s bundle identifier you set earlier. Next, TILViewModel() simply renames JavaScript’s this to self , which is more familiar to iOS developers, and gets the default container. The last line applies Knockout bindings: there aren’t any bindings yet, but this also creates and runs TILViewModel() .

Create an API Token

Note: If you’re not an Apple Developer, leave my container identifier and API token as they are in TIL.js and advance to the Error Codes section. If you want to create a web app to access someone else’s CloudKit container, you’ll need their container identifier and a fresh API token for your web app — see below.

In the CloudKit dashboard, create an API token for your container. Select Admin\API Access and then Add new API token. Name it JSToken, check Discoverability and click Save.

Copy the token.

Back in TIL.js, paste your new token in place of the current apiToken . Leave environment set to development and save.

Error Codes

TIL.js contains console.log statements that perform the same role as debug print statements in Swift. The messages appear in the browser’s console — Safari’s Error Console, Chrome’s JavaScript Console, or Firefox’s Browser Console.

Showing the Safari Error Console

Open index.html in Safari. To show the error console, select Safari\Preferences\Advanced, and check Show Develop menu in menu bar.

Close the preferences window and select Develop\Show Error Console. Refresh the page to see console.log messages.

Note: If you’re using Chrome, its console is in the View\Developer menu. If you’re using Firefox, find it under the Tools\Web Developer menu.

Querying the Public Database

Anyone can view the public database, even without signing in to iCloud, as long as you fetch the public records and display them on a webpage. That’s what you’ll do in this section.

In TIL.js, add the following to TILViewModel() , right before the method’s ending curly brace:

console.log("set publicDB"); var publicDB = container.publicCloudDatabase; self.items = ko.observableArray();

This little block gets you a reference to the public database via publicDB then it declares items as a Knockout observable array that contains the public records.

Add the following right after the lines you just added:

// Fetch public records self.fetchRecords = function() { console.log("fetching records from " + publicDB); var query = { recordType: 'Acronym', sortBy: [{ fieldName: 'short'}] }; // Execute the query. return publicDB.performQuery(query).then(function(response) { if(response.hasErrors) { console.error(response.errors[0]); return; } var records = response.records; var numberOfRecords = records.length; if (numberOfRecords === 0) { console.error('No matching items'); return; } self.items(records); }); };

Here you define the fetchRecords function, which retrieves the public records sorted by the short field and stores them in items .

Note: If you want to create a web app to access someone else’s CloudKit container, you need to know how its databases are organized, namely record types, field names and field types. TIL’s public database stores records of type Acronym, which has two String fields named short and long.

Lastly, add the following right after the previous lines:

container.setUpAuth().then(function(userInfo) { console.log("setUpAuth"); self.fetchRecords(); // Don't need user auth to fetch public records });

Here you run container.setUpAuth() to check whether a user is signed in, and then it presents the appropriate sign-in/out button. You don’t need authentication yet, but you still call fetchRecords() to get the Acronyms to display.

Save TIL.js.

Next, open index.html and scroll to the bottom. Add the following right above the End Document comment:

<div data-bind="foreach: items"> <div class="row"> <div class="three columns"> <h5><span data-bind="text: fields.short.value"></span></h5> </div> <div class="nine columns"> <p><span data-bind="text: fields.long.value"></span></p> </div> </div> </div>

Here you iterate through the items array. Each element in items is an acronym record.

Knockout’s text binding displays the short and long text values. The foreach control flow binding duplicates the Skeleton row for each element in items , and it binds each row to the corresponding items element.

Because items is an observable array, this binding efficiently updates the displayed rows every time items changes.

Save index.html and reload it in the browser. Public database records appear in the browser window and console.log messages still show in the error console.

Note: Because there are no signed in users, a 421 error appears when you initialize publicDB . Don’t worry, this won’t stop the list from appearing. Select Logs instead of All to see only log messages. If the list doesn’t appear, check the console messages to see if CloudKit failed to load, and if so, reload the web page.

Authenticating iCloud Users

To add items to the public database, users must sign in to iCloud. Apple handles user authentication directly and provides sign-in and sign-out buttons. If a user has no Apple ID, the sign-in dialogue lets them create one.

In TIL.js, add this code to the end of container.setUpAuth() , just below the call to fetchRecords() :

if(userInfo) { self.gotoAuthenticatedState(userInfo); } else { self.gotoUnauthenticatedState(); }

container.setUpAuth() is a JavaScript promise — the outcome of an asynchronous task. In this case, the task determines whether there’s an active CloudKit session with an authenticated iCloud user. When the task finishes, the promise resolves to a CloudKit.UserIdentity dictionary or null , or it rejects to a CloudKit.CKError object.

When the promise resolves, the CloudKit.UserIdentity dictionary becomes available to the then function as the parameter userInfo . You just added the body of the then function; if userInfo isn’t null , you pass it to gotoAuthenticatedState(userInfo) ; otherwise, you call gotoUnauthenticatedState() .



Now, you’ll define these two functions, starting with gotoAuthenticatedState(userInfo) .

Add these lines right above container.setUpAuth().then(function(userInfo) { :

self.displayUserName = ko.observable('Unauthenticated User'); self.gotoAuthenticatedState = function(userInfo) { if(userInfo.isDiscoverable) { self.displayUserName(userInfo.firstName + ' ' + userInfo.lastName); } else { self.displayUserName('User Who Must Not Be Named'); } container .whenUserSignsOut() .then(self.gotoUnauthenticatedState); };

Because you checked Request user discoverability at sign in when you created the API key, users can choose to let the app know their names and email addresses.

If the user isDiscoverable , the web page will display their name. Otherwise, they’ll be called the User Who Must Not Be Named; while you could display the unique userInfo.userRecordName returned by the iCloud sign-in, that’d be far less amusing. ;]

Either way, iCloud remembers the user’s choice and doesn’t ask again.

container.whenUserSignsOut() is another promise — its then function calls gotoUnauthenticatedState() .

Right after the code you just inserted, add the following to define gotoUnauthenticatedState() :

self.gotoUnauthenticatedState = function(error) { self.displayUserName('Unauthenticated User'); container .whenUserSignsIn() .then(self.gotoAuthenticatedState) .catch(self.gotoUnauthenticatedState); };

When no user is signed in, you reset displayUserName and wait for a user to sign in. If the container.whenUserSignsIn() promise rejects to an error object, the app remains in an unauthenticated state.

Save TIL.js, and go back to index.html in Xcode.

Add the following right after <h2>TIL: Today I Learned <small>CloudKit Web Service</small></h2> :

<h5 data-bind="text: displayUserName"></h5> <div id="apple-sign-in-button"></div> <div id="apple-sign-out-button"></div>

The h5 header creates a text binding to the observable displayUserName property in TIL.js. To fulfill its promise, container.setUpAuth() displays the appropriate sign-in/out button.

Save and reload index.html to see Unauthenticated User and the sign-in button.

Click the sign-in button and login to an iCloud account. Any Apple ID works; it need not be an Apple Developer account.

After you sign in, the web page will update displayUserName and display the sign-out button.

Sign out of iCloud before continuing — the next step will clear userInfo and display the sign-in button. You’ll feel more in control if you sign out now. :]

Updating the Public Database

You need a web form where users can add new items and some corresponding JavaScript that’ll save items to the public database.

In TIL.js, right after the fetchRecords() definition, add these lines:

self.newShort = ko.observable(''); self.newLong = ko.observable(''); self.saveButtonEnabled = ko.observable(true); self.newItemVisible = ko.observable(false);

Here you declare and initialize observable properties that you’ll bind to the UI elements in index.html.

There will be two input fields, newShort and newLong and a submit button that you’ll disable when saving the new item.

Only authenticated users can add items to the database, so newItemVisible controls whether the new-item form is visible. Initially, it’s set to false .

Add this line to the gotoAuthenticatedState function right after its open curly brace:

self.newItemVisible(true);

This block makes the new-item form visible after a user signs in.

Add this to the top of the gotoUnauthenticatedState function:

self.newItemVisible(false);

In here, you’re hiding the new-item form when the user signs out.

Next, add the following to define the saveNewItem function, right below the self.newItemVisible = ko.observable(false); line:

self.saveNewItem = function() { if (self.newShort().length > 0 && self.newLong().length > 0) { self.saveButtonEnabled(false); var record = { recordType: "Acronym", fields: { short: { value: self.newShort() }, long: { value: self.newLong() }} }; publicDB.saveRecord(record).then(function(response) { if (response.hasErrors) { console.error(response.errors[0]); self.saveButtonEnabled(true); return; } var createdRecord = response.records[0]; self.items.push(createdRecord); self.newShort(""); self.newLong(""); self.saveButtonEnabled(true); }); } else { alert('Acronym must have short and long forms'); } };

This checks that input fields are not empty, disables the submit button, creates a record and saves it to the public database. The save operation returns the created record, which you push (append) to items instead of fetching all the records once again. Lastly, you clear the input fields and enable the submit button.

Save TIL.js, and return to index.html in Xcode.

Add the following right before <div data-bind="foreach: items"> :

<div data-bind="visible: newItemVisible"> <div class="row"> <div class="u-full-width"> <h4>Add New Acronym</h4> </div> </div> <form data-bind="submit: saveNewItem"> <div class="row"> <div class="three columns"> <label>Acronym</label> <input class="u-full-width" placeholder="short form e.g. FTW" data-bind="value: newShort"> </div> <div class="nine columns"> <label>Long Form</label> <input class="u-full-width" placeholder="long form e.g. For the Win" data-bind="value: newLong"> <input class="button-primary" type="submit" data-bind="enable: saveButtonEnabled" value="Save Acronym"> </div> </div> </form> <hr> </div>

The heart of this code is a web form with two input fields — short gets three columns and long gets nine. You also created a submit button that’s left-aligned with the long input field. Whenever the submit button is tapped, saveNewItem is invoked.

For the value bindings, you name the observable properties newShort and newLong that saveNewItem uses. The visible binding will show or hide the web form, according to the value of the observable property newItemVisible . Lastly, the enable binding enables or disables the submit button, according to the value of the observable property saveButtonEnabled .

Save the file and reload index.html in a browser.

Sign in to an iCloud account and the fancy new form should show itself. Try adding a new item, such as YOLO/You Only Live Once:

Click Save Acronym and watch your new item appear at the end of the list!

Go back to the CloudKit Dashboard \ Default Zone and change the sort order to see your new item appear.

Getting Notification of Changes to the Public Database

When users add or delete items the web page should update to show the current list. Like any respectable CloudKit iOS app, your app can subscribe to the database for updates.

In TIL.js, add the following inside the gotoAuthenticatedState function, right after self.newItemVisible(true) :

//1 var querySubscription = { subscriptionType: 'query', subscriptionID: userInfo.userRecordName, firesOn: ['create', 'update', 'delete'], query: { recordType: 'Acronym', sortBy: [{ fieldName: 'short'}] } }; //2 publicDB.fetchSubscriptions([querySubscription.subscriptionID]).then(function(response) { if(response.hasErrors) { // subscription doesn't exist, so save it publicDB.saveSubscriptions(querySubscription).then(function(response) { if (response.hasErrors) { console.error(response.errors[0]); throw response.errors[0]; } else { console.log("successfully saved subscription") } }); } }); //3 container.registerForNotifications(); container.addNotificationListener(function(notification) { console.log(notification); self.fetchRecords(); });

Here’s what you set up:

Subscriptions require users to sign in because they use per-user persistent queries, so you set subscriptionID to userInfo.userRecordName . To avoid firing off the error message triggered by saving a pre-existing subscription, you attempt to fetch the user’s subscription first. If the fetch fails then the subscription doesn’t exist, so it’s safe to save it. You register for notifications and add a notification listener that calls fetchRecords() to get the new items in the correct sorted order.

Save TIL.js, reload index.html in a browser and sign in. Add a new acronym (perhaps AFK/Away From Keyboard) in the CloudKit dashboard, another browser window, or in the iOS app.

The notification appears in the console and the list of updates on the page! Magic!

Handling Race Conditions

Sometimes the list doesn’t update, even when the notification appears and fetchRecords successfully completes.

The reason this happens is that race conditions are possible with asynchronous operations, and fetchRecords sometimes runs before the new item is ready. Try printing records.length to the console at the end of the performQuery(query) handler so you can see that this number doesn’t always increase after a notification.

You can mitigate this risk by replacing the first class="row" div of index.html with the code below to provide a manual refresh button:

<div class="row"> <div class="u-full-width"> <h2>TIL: Today I Learned <small>CloudKit Web Service</small></h2> </div> </div> <div class="row"> <div class="six columns"> <h5 data-bind="text: displayUserName"></h5> </div> <div class="four columns"> <div id="apple-sign-in-button"></div> <div id="apple-sign-out-button"></div> </div> <div class="two columns"> <div><button data-bind="click: fetchRecords">Manual Refresh</button></div> </div> </div>

Save and reload index.html to see the new button. By the way, it even works without signed in users:

Bonus: Server-Side CloudKit Access

On February 5, 2016 — a week after Facebook announced plans to retire the Parse service — Apple announced that CloudKit now supports server-to-server web service requests, enabling reading and writing to CloudKit databases from server-side processes or scripts.

Talk about a Big Deal: now you can use CloudKit as the backend for web apps that rely on admin processes to update data — like most modern web apps.

However, the API key isn’t enough. You need a server key.

Note: If you’re not in an Apple Developer Program, you won’t be able to do this part of the tutorial because it requires my private key. At least read through it so you’re familiar with the process of implementing this excellent feature.

Open Terminal, cd to the TIL Starter/Server directory, and enter this:

openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem

This creates a server-to-server certificate: eckey.pem contains the private key.

Still in Terminal, enter the following to display the new certificate’s public key:

openssl ec -in eckey.pem -pubout

In the CloudKit Dashboard, navigate to API Access\Server-to-Server Keys and click Add Server-to-Server Key.

Name the key Server2ServerKey.

Copy the public key from Terminal’s output, paste it into Public Key and tap Save. Then, copy the generated Key ID.

Open config.js in Xcode, and replace my containerIdentifier and keyID with your own:

module.exports = { // Replace this with a container that you own. containerIdentifier:'iCloud.com.raywenderlich.TIL', environment: 'development', serverToServerKeyAuth: { // Replace this with the Key ID you generated in CloudKit Dashboard. keyID: '1f404a6fbb1caf8cc0f5b9c017ba0e866726e564ea43e3aa31e75d3c9e784e91', // This should reference the private key file that you used to generate the above key ID. privateKeyFile: __dirname + '/eckey.pem' } };

To run index.js from a command line, you’ll need to complete a few more steps.

First, go to nodejs.org and install Node.js on your computer if you don’t have it. Next, follow the advice at the end of the setup and add /usr/local/bin to your $PATH, if it’s not there already.

Back in Terminal and still in the TIL Starter/Server directory, run these commands:

npm install npm run-script install-cloudkit-js

These commands install the npm module and the CloudKit JS library, which index.js uses.

Now, enter this command in Terminal to run index.js:

node index.js

The output of this command looks similar to the following:

CloudKitJS Container#fetchUserInfo --> userInfo: a { userRecordName: '_a4050ea090b8caace16452a2c2c455f4', emailAddress: undefined, firstName: undefined, lastName: undefined, isDiscoverable: false } CloudKitJS CloudKit Database#performQuery { recordType: 'Acronym', sortBy: [ { fieldName: 'short' } ] } {} --> FOMO: Fear Of Missing Out Created Sun Jun 19 2016 20:16:32 GMT+1000 (AEST) ... --> YOLO: You Only Live Once Created Fri Jun 17 2016 14:37:04 GMT+1000 (AEST) Done

In here you’ve adapted config.js and the index.js from Apple’s CloudKit Catalog source code to query the TIL public database and print the short , long and created fields.

Where to Go From Here?

Here’s the final version of the web app.

You covered quite a bit in this CloudKit JS tutorial and know the basics of how to use CloudKit JS to make your iOS CloudKit app available to a wider audience via a web interface.

Using CloudKit JS to access CloudKit Web Services Viewing JavaScript log messages in the browser’s console Querying the public database to make it visible to everyone Authenticating users through iCloud Building the web UI to facilitate new entries Handling notifications of changes to keep everything in sync Supporting server-to-server requests for CloudKit databases

Watch CloudKit JS and Web Services from WWDC 2015, and take some of the features in CloudKit Catalog for a test drive. Explore additional features like user discoverability, record zones and syncToken .

Watch What’s New with CloudKit from WWDC 2016 for an in-depth look at record sharing and the new record sharing UI — you can try this out in CloudKit Catalog, too. Apple keeps refining CloudKit to make it easier for developers to create reliable apps: look at CKOperation ‘s QualityOfService to handle long-running operations and CKDatabaseSubscription and CKFetchDatabaseChanges to get changes to record zones that didn’t even exist when your app started!

I hope you enjoyed this tutorial — I sure had fun putting it together! Please join the discussion below to share your observations, feedback, ask questions or share your “ah-ha” moments!