
Learn how to access CloudKit databases from a web app in this CloudKit JS tutorial!
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 stuff’s 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, web services topped the list of feature requests. Then in 2015, Apple announced CloudKit Web Services, a JSON/HTTPS interface for CloudKit.
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.
- 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. 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, sowindow.addEventListener('cloudkitloaded', function() { ... }
attaches thefunction
as the event handler of thecloudkitloaded
event. The rest of the code in TIL.js is thecloudkitloaded
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’sthis
toself
, 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
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.
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
touserInfo.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.
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!
The post CloudKit JS Tutorial for iOS appeared first on Ray Wenderlich.