CloudKit JS Tutorial for iOS

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! By Audrey Tam.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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:

  1. Subscriptions require users to sign in because they use per-user persistent queries, so you set subscriptionID to userInfo.userRecordName.
  2. 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.
  3. 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!

browser08

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:

browser09

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.

dashboard08

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.

dashboard09

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.

  1. Using CloudKit JS to access CloudKit Web Services
  2. Viewing JavaScript log messages in the browser’s console
  3. Querying the public database to make it visible to everyone
  4. Authenticating users through iCloud
  5. Building the web UI to facilitate new entries
  6. Handling notifications of changes to keep everything in sync
  7. 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!