MacKuba

Kuba Suder's blog on Mac & iOS development

Notes

App ClipsAppKitCloudKitExtensionsFoundationiCloudLocationLoggingMacMapsPerformancePhotosPrivacySafariSwiftSwiftUIUIKitWatchKitWWDC 12WWDC 14WWDC 15WWDC 16WWDC 18WWDC 19WWDC 20

CloudKit Best Practices

Categories: CloudKit, iCloud, WWDC 16 0 comments Watch the video

Short CloudKit overview

Apple uses CloudKit in their applications, so you can be confident that it scales, because for Apple it scales to hundreds of millions of users

CloudKit lets you focus on building your applications and not worry about building backend services for them

It provides your users automatic authentication – if the user is logged in to iCloud on their device, they don’t need to log in separately in your app

A CloudKit container now includes 3 databases:

  • public database for data visible to everyone
  • private database for a given user’s private data
  • new this year: shared database for user data that they decided to share with others

Zones:

  • public database has 1 default zone
  • private database has a default zone and it can have one or more custom zones
  • shared database includes some number of shared zones

A record always exists in a specific zone

Building an app with a sync feature

A common use case (e.g. Notes app):

  • user creates some data/records/documents on one of their devices
  • later, they open another device and they expect to see these documents there and be able to read/edit them

The way this is implemented is that CloudKit needs to be the source of truth, and the devices should maintain a local cache of all the app data and synchronize it using CloudKit

The recommended workflow:

  1. 1. On app launch, fetch changes from the server
  2. 2. Subscribe to any future changes
  3. 3. Fetch changes when you receive a push

Subscriptions:

Subscriptions let you ask the server to notify you whenever a change happens in the specified set of data. Previously you could subscribe to a specific query to a record type or to all changes in a zone.

New in iOS 10 – CKDatabaseSubscription – lets you subscribe to all changes in the whole database (private or shared).

Types of subscription notifications:

  1. 1. Silent push:
let notificationInfo = CKNotificationInfo()

// we only set this, but none of the UI related keys
notificationInfo.shouldSendContentAvailable = true

// do this once. no need to ask the user for push notifications permission,
// since we won't show any visible notifications
application.registerForRemoteNotifications(…)
  1. 2. Visual notification:
let notificationInfo = CKNotificationInfo()

// set any of these
notificationInfo.shouldBadge = true
notificationInfo.alertBody = "alertBody"
notificationInfo.soundName = "default"

// we need to prompt the user for push notification access:
application.registerUserNotificationSettings(…)
application.registerForRemoteNotifications(…)

Remember that push notifications can be coalesced, so you may only get one out of a series. Push notifications tell you that *something* has changed, but not necessarily every single thing that has changed.

Creating a subscription:

This only needs to be done the first time you launch an app – so we set a flag when we create a subscription and the next time we skip this part.

if subscriptionIsLocallyCached { return }

let subscription = CKDatabaseSubscription(subscriptionID: "shared-changes")

let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo

let operation = CKModifySubscriptionsOperation(
    subscriptionsToSave: [subscription],
    subscriptionIDsToDelete: []
)

operation.modifySubscriptionsCompletionBlock = { …
    if error != nil {
        …
    } else {
        self.subscriptionIsLocallyCached = true
    }
}

operation.qualityOfService = .utility
self.sharedDB.add(operation)

Listening for pushes:

  • turn on “Remote notifications” and “Background fetch” capabilities
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [NSObject: AnyObject],
    fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {

    let dict = userInfo as! [String: NSObject]
    let notification = CKNotification(fromRemoteNotificationDictionary: dict)

    if notification.subscriptionID == "shared-changes" {
        fetchSharedChanges {
              completionHandler(.newData)
        }
    }
}

Fetching the changes:

Steps:

  • ask in which zones something was changed (in shared db – because there may be new zones added when a new user shares some content)
  • ask which records have changed in each relevant zone

The server will not send you pushes about the changes you’re doing on this device, but you may receive those changes you’ve done on the list when fetching a delta download

fetchAllChanges: previously, in some operations you had to manually check for a flag that says there is more results waiting for you that you need to manually request (i.e. another page)

Now, CloudKit does the paging automatically for you if fetchAllChanges = true (which is the default)

func fetchSharedChanges(_ callback: () -> Void) {
    let changesOperation = CKFetchDatabaseChangesOperation(
        previousServerChangeToken: sharedDBChangeToken  // cached between runs
    )

    // this gives you IDs of changed zones
    changesOperation.recordZoneWithIDChangedBlock = { … }

    // this gives you IDs of deleted zones
    changesOperation.recordZoneWithIDWasDeletedBlock = { … }

    // this gives you the current change token which you need to save
    // may be called multiple times if the operation fetches multiple pages of content
    // save the token each time, so in case of an error you don"t repeat all work
    changesOperation.changeTokenUpdatedBlock = { … }

    changesOperation.fetchDatabaseChangesCompletionBlock = {
        (newToken: CKServerChangeToken?, more: Bool, error: NSError?) -> Void in

        self.sharedDBChangeToken = newToken
        self.fetchZoneChanges(callback)
    }

    self.sharedDB.add(operation)
}

fetchZoneChanges looks very similar, but fetches changes for a specific zone using CKFetchRecordZoneChangesOperation (you pass it a list of zones)

CloudKit best practices:

Automatic authentication:

CloudKit allows you to authenticate users (if they’re logged in to iCloud) without requiring any private information

You use the CloudKit user record for authentication

The user record is unique per container and never changes for that user

container.fetchUserRecordID(completionHandler: (CKRecordID?, NSError?) -> Void)

CKOperation API:

The convenience API works on single items and it’s simpler to use

Every convenience API call has a CKOperation counterpart that lets you perform an operation on a batch of records

The CKOperation also has other advantages – for example, it lets you:

  • set up dependencies between operations
  • specify quality of service and queue priorities
  • cancel operations that have started executing
  • specify if you want the operation to work over cellular network
  • limit the number of records or set of fetched keys
  • report progress
  • … and everything that NSOperation provides

(*) watch the Advanced NSOperations talk from 2015 to learn more about NSOperation

Quality of service:

QoS: select a quality of service (.userInteractive / .userInitiated / .utility / .background) depending on the task priority

  • default is .utility
  • .utility and below enable discretionary networking

Discretionary networking means that:

  • the system decides when is the best moment to run your request, so it may take longer than you expect
  • however, all network failures will be automatically retried for you
  • the request gets a timeout period of 7 days by default

Long lived operations:

If you have some operations that you want to continue/retry if they don’t manage to complete by the time your app is terminated, iOS 9.3 adds “CloudKit long lived operations”

Once you run such operation, the system will finish it even if the app is killed by the system or the user

The request is executed even if your app isn’t running, the result is cached and is returned to you once the app restarts

Results are kept by the OS for at least 24 hours

To use this API:

  • set isLongLived = true on CKOperation
  • save the operation’s operationID
  • use CKContainer.fetchLongLivedOperation(withId:) to get the operation object back
  • set completion blocks and run it again just like a new one
CKContainer.default().fetchLongLivedOperation(withID: myOpID) {
    (operation: CKOperation?, error: NSError?) in

    let fetchRecords = operation as! CKFetchRecordsOperation
    fetchRecords.fetchRecordsCompletionBlock = { … }

    CKContainer.default().privateCloudDatabase.add(fetchRecords)
}

Parent references:

A new type of reference added this year to help you better model data, especially with sharing in mind

If your app supports sharing, it’s recommended that you set the parent reference to create a hierarchy between records

Example: Album  ⭢  list of photos

let photoRecord = CKRecord(recordType: "photo")
photoRecord.setParent(albumRecordID)

What this gives you: when the user shares the album record, the whole record hierarchy under this album (photos and other data) will also be shared

Types of errors:

1) Fatal error (bad request)

Error codes like:

  • .internalError
  • .serverRejectedRequest
  • .invalidArguments
  • .permissionFailure

→ in this case, you should show an alert to the user and tell them this can’t be executed

2) Connection/server error

Error codes like:

  • .zoneBusy
  • .serviceUnavailable
  • .requestRateLimited

→ in this case, check for CKErrorRetryAfterKey and retry after specified time

3) Errors that are returned before connection is even made

.networkUnavailable

  • you should monitor network reachability (SCNetworkReachability) and retry when the device is connected again

.notAuthenticated

  • when the user is not logged in and can’t access their private database
  • you should register at startup for CKAccountChangedNotification, and when it fires, recheck account status and update the UI

CloudKit Tips and Tricks

Categories: CloudKit, iCloud, WWDC 15 0 comments Watch the video

Error Handling

Accounts:

To check the account status of the current user:

container.accountStatusWithCompletionHandler { status, error in … }

All APIs that fail because they require an authenticated user return CKErrorNotAuthenticated

You can now subscribe for CKAccountChangedNotification to be notified when account status changes

You should avoid showing alerts to the user about a missing account – simply disable parts of the UI that require an account, and reenable them when you get a notification that an account is now available

Network errors:

Network connection errors that you may sometimes get: CKErrorNetworkFailure, CKErrorServiceUnavailable, CKErrorZoneBusy, CKErrorRequestRateLimited

These errors include a key CKErrorRetryAfterKey in their user info dictionary that tells you how long you should wait before retrying

Handling conflicts:

If you try to save a record that has been modified in the meantime on the server (meaning: the record change tag you’re sending with the save request is outdated), you will receive the error CKErrorServerRecordChanged

There is no magic happening behind the scenes in such case, iCloud doesn’t make assumptions about how you want to resolve conflicts, you need to handle this yourself

However, the SDK provides you the necessary information in the userInfo:

  • CKRecordChangedErrorClientRecordKey – what you tried to save
  • CKRecordChangedErrorAncestorRecordKey – the original version
  • CKRecordChangedErrorServerRecordKey – what is currently on the server

Usually you will want to resolve the conflict by applying the same changes that you did on the original record to the current server version of the record

CloudKit Operations

Batch operations:

If you create and save a lot of records in one go, each of them will create a separate network request, and making a lot of requests in a short period means you’re likely to hit some kind of rate limit and they will be queued up

To avoid making multiple similar requests, you can use the CKOperation API

Almost every convenience API method that works on one record at a time has a CKOperation counterpart that allows you to work on a batch of records

For saving multiple records, use CKModifyRecordsOperation:

class CKModifyRecordsOperation: CKDatabaseOperation {
  convenience init(recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecordID]?)
}

Note: there are certain limits on how large batch operations you can make (number of items in the request and total request size)

This doesn’t include the size of saved binary assets, just the record field data

If you hit this limit, you will get the CKErrorLimitExceeded error

In that case, the best solution is usually to try to divide the batch in half and make two requests

If one or more records in the batch can’t be saved, you will get CKErrorPartialFailure

From this error’s userInfo you can get a dictionary with specific record errors under CKPartialErrorsByItemIDKey

In a standard zone, in such scenario some records will be saved and those with errors won’t

In a custom zone you can make an atomic update – in this case, in case of a problem with some of the records, no records will actually be saved, but instead you will get an error CKErrorBatchRequestFailed for those that could have been saved but weren’t

Queries:

If you expect a query to return a large number of records, but you only need a small number of them at a time, you can use the CKQueryOperation.resultsLimit property

Also available on CKFetchRecordChangesOperation, CKFetchNotificationChangesOperation

When limiting the number of records, you will usually also want to set the query sortDescriptor to e.g. sort records by oldest or newest first

You can use the creationDate key which is automatically added to all saved records regardless of type

To implement pagination and get further pages beyond the first one, use the CKQueryCursor object that you get in response to the queryCompletionBlock callback

Then, initialize the next CKOperation passing it the cursor object in the argument to the initializer

If you don’t need all information contained in a record immediately, you can also use the desiredKeys property to only download the keys you want

E.g. download the record’s thumbnail image but not a full-size photo

Also available on CKFetchRecordsOperation, CKFetchRecordChangesOperation

Maintaining a local cache:

We want to keep some subset of all data completely cached on all local devices for quicker access (e.g. user’s personal notes)

You have two options:

  • use CKQueryOperation to fetch all records and synchronize them manually
  • make a custom zone and use delta downloads using CKFetchRecordChangesOperation

When saving fetched records to a local database, you should save CKRecord’s system fields like the change tag together with your own data

To do that, you can use encodeSystemFieldsWithCoder:

let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
archiver.requiresSecureCoding = true
record.encodeSystemFieldsWithCoder(archiver)
archiver.finishEncoding()

When restoring a record from the local storage, you don’t have to set all its data fields – it’s fine to only set those you want to change

To synchronize any changes from the server, create a subscription subscribing to a given record type using silent notifications, and use CKFetchRecordChangesOperation to fetch all recent changes when notified

Subscriptions (CKSubscription) are persistent queries on the server that send remote notifications about a relevant change – either in a specific record set (query subscription) or in the whole zone (zone subscription)

To get CloudKit subscription notifications, you need to follow the usual setup for push notifications:

  • have push notification capability enabled for your app
  • call registerForRemoteNotifications()
  • call registerUserNotificationSettings(…) if you want to show notifications to the user

To ask for silent subscription notifications, configure the CKNotificationInfo object appropriately:

  • set the shouldSendContentAvailable key
  • do not set any of the UI-related keys: alertBody, shouldBadge, soundName

Notification priorities: a notification is high priority if it has any UI keys set, otherwise it’s medium priority

For silent notifications, the UIApplicationDelegate will receive the following callback:

func application(application: UIApplication,
	didReceiveRemoteNotification: [NSObject: AnyObject],
	fetchCompletionHandler: (UIBackgroundFetchResult) -> Void) { … }

Remember that push notification delivery in general is “best effort” – pushes can be dropped if many are received in a short period of time or because of network issues

Silent notifications may also be additionally delayed if the system is waiting for better conditions

When you receive a notification, use CKFetchNotificationChangesOperation to check the server’s notification collection for any notifications you might have missed

You may want to use a UIApplication background task (beginBackgroundTaskWithName(…)) for syncing tasks

Interactive notifications: you can now make CloudKit notifications interactive (e.g. show action buttons) by setting the category key on CKNotificationInfo

Other performance tips

CloudKit is a highly asynchronous API, most operations require a network call and take some time to execute

You will often want to make a series of operations that have some dependencies between them

Things to keep in mind:

  • however you implement task handling, remember to always handle all errors
  • never block the main thread with an operation in progress

Don’t nest calls to the convenience API methods, creating a “callback hell”

Don’t use locks/semaphores to wait for an API call to finish

Instead, use the addDependency() API in CKOperation to add dependencies between operations:

let firstFetch = CKFetchRecordsOperation(…)
let secondFetch = CKFetchRecordsOperation(…)
secondFetch.addDependency(firstFetch)

let queue = NSOperationQueue()
queue.addOperations([firstFetch, secondFetch], waitUntilFinished: false)

Use the qualityOfService property on NSOperation to indicate which operations are something you need in the UI and which are low priority background operations

  • there used to be a usesBackgroundSession property on CKOperation too, but it’s deprecated now  ⭢  use quality of service for this

QoS = .utility and .background use discretionary networking, use .userInteractive and .userInitiated for high priority tasks

Note: .background QoS is the default if you don’t change it!

  • update: now it's .utility

Advanced CloudKit

Categories: CloudKit, iCloud, WWDC 14 0 comments Watch the video

CloudKit API is designed to be asynchronous, all calls return through a callback, because they all require a network connection

The main API (“operational API”) is based on NSOperation

You use it by creating special NSOperation objects for a given use case, e.g. CKFetchRecordsOperation, and specifying parameters and callbacks in its properties

Apart from the final result callback, you can set callbacks e.g. for reporting download progress or to get records one by one as they’re downloaded

Operation lifecycle (cancelling, suspending etc.) can be managed through standard NSOperation methods and NSOperationQueue

There are separete fetch/modify operation types for records, subscriptions, zones, users and notifications

You can set dependencies between operations (also if they’re in different queues), e.g. make a fetch operation and then a modify operation that needs to wait for the object to load

Operations can also have different priority levels

Starting an operation:

(ℹ️ Note: this wasn’t in the video, but it really should have been, because it's completely not obvious.)

How to start an operation once you prepare the CKOperation object:

1) Use the database’s built-in queue:

let fetchOperation = ...
CKContainer.default().privateCloudDatabase.add(fetchOperation)

2) Use your own operation queue and assign a reference to the database:

let operationQueue = NSOperationQueue()

let fetchOperation = ...
fetchOperation.database = CKContainer.default().privateCloudDatabase
operationQueue.addOperation(fetchOperation)

Custom zones

Custom zones (in the private database) let you compartmentalize data and add some special features

Records can’t be moved between zones or have cross-zone relationships

There are some operations that can only be done in custom zones:

Atomic commits:

Objects in the CloudKit database have relationships between them, and you want to keep all data consistent

Atomic commits are kind of like transactions in a relational database: batch operations succeed or fail together

Only available in the private database (because public database may be accessed by millions of users at the same time)

If an operation fails, you get a CKErrorPartialFailure response, with the user info containing info about errors on specific records (CKPartialErrorsByItemID)

Error CKErrorBatchRequestFailed means that this record wasn’t saved because of a problem with another record in the batch

Delta downloads:

Allows you to download a list of all changes since the last time the app was online, to let you perform a full sync

When a device connects, you can send a “change token” to the server asking for all changes since that version

This lets you implement an offline cache of the whole dataset and sync any changes when possible

To do that:

  • track all local changes
  • send changes to the server when connected
  • resolve conflicts
  • fetch server changes with CKFetchRecordChangesOperation
  • remember the received new server change token and send it back next time

Zone subscriptions:

Lets you subscribe for notifications about any change in the zone

When you get a notification, you request a delta download

Advanced record operations

Record changes:

When you change some fields in a CKRecord, the changes are automatically tracked locally and only the changed fields are transmitted when you save it

By default CloudKit performs a “locked update”, which makes sure that the update is only saved on the server if the record wasn’t modified in the meantime by another client (this uses record change tokens)

After you execute a save, the server returns your record with a new change token – so you should use that returned version for any subsequent changes

Unlocked update  ⭢  just overwrites server data regardless what is there

Locked update  ⭢  if the record was changed in the meantime, you get back an error (CKErrorServerRecordChanged)

The userInfo of the CKErrorServerRecordChanged error contains info that lets you perform a 3-way merge:

  • CKRecordChangedErrorClientRecordKey – what you tried to save
  • CKRecordChangedErrorAncestorRecordKey – the original version
  • CKRecordChangedErrorServerRecordKey – what is currently on the server

Based on the values from these 3 copies of the record you can decide what state the record should be in, and then retry the save

You can modify the behavior with “save policies”:

  • SaveIfServerUnchanged  ⭢  default, performs a locked update and sends only changed keys
  • SaveChangedKeys  ⭢  unlocked update, sends only changed keys
  • SaveAllKeys  ⭢  unlocked update, overwrites all keys in the record (note: this doesn’t affect keys that aren’t present in the local copy at all)

You should almost always use the default locked update (SaveIfServerUnchanged), use unlocked updates only to forcefully resolve serious conflicts

Use SaveAllKeys if the user requests to overwrite server data with local data

Partial records:

The desiredKeys field present in most operation types lets you specify that you only want to download selected keys from the server

This is useful if the whole record is very large and you don’t need all of it

Partial records can be normally saved after a change

CloudKit data modeling

References:

Forward reference  ⭢  a parent object keeps an array of references to children in its property

Backward reference  ⭢  only child objects have a reference to the parent

It’s recommended to use backward references – with a forward reference you need to update the parent object every time a new child is added, and you will run into conflicts if multiple clients are adding records

To get a list of all children using backward references, make a query for all child records with a predicate “owner = X”

References give you cascading deletes – when you delete the parent object, all child objects and their children are deleted

If an object has two parent references, it’s deleted when the first parent is deleted

When batch uploading a tree of objects, CloudKit makes sure that parent objects are uploaded first so that you don’t get inconsistent data during upload (important in the public database)

Your data objects:

CloudKit is only a transport mechanism and requires you to keep and manage your own local copy of all data

It’s recommended that you don’t subclass CK* objects to build your models – make your own completely independent model classes and translate to/from CloudKit objects when fetching and saving

Handling push notifications:

You need to remember that push notifications in general aren’t guaranteed to be delivered

The server only stores one push per client, so if you reconnect e.g. after a flight, you might miss some previous notifications

You can find pushes that you’ve missed in a “Notification Collection” where every notification is saved

The Notification Collection works kind of like delta updates – you ask for notifications since a given change token and you get a list of everything added since then

You can mark a notification as read, which notifies all other clients that they can ignore it

You should check the Notification Collection every time you get a push, since you never know what you might have missed (this doesn’t only happen with airplane mode)

The iCloud Dashboard

The dashboard lets you browse data saved by your app – the whole public database and the private database for your developer account (but not anyone else’s private database)

You can view saved records, run queries with any filters, and add new records

You can define roles in the public database and define for each model who can create/read/modify records (e.g. specify that records are publicly readable but only an admin can create them)

You will also see a list of all user ids and first/last names of those users that marked themselves as discoverable

Schema:

The CloudKit database has two separate “environments”: development and production

The schema for each record type is “just in time” during development, i.e. when you save a new type of record, it automatically creates a new schema for that record type, recording every field type, and when you save a record with a new field, it adds a field to the list

However, once you’re ready to release a new version of your app, you need to save the schema to production and at that point it’s locked – a production version of the app can’t save records or fields that aren’t defined in the schema

CloudKit also automatically creates indexes for each field in each record type – when you’re done with development, you can delete some indexes that you won’t need so they don’t waste space in the production database

Tips & tricks

Please handle all errors :)

Remember that you can get partial errors (when atomic commits aren’t used), so some records might be saved while others aren’t

Retry any “server busy” errors (CKErrorRetryAfterKey tells you the amount of time you should wait)

Don’t waste space in your users’ iCloud in private databases, they may be paying real money for it

Limits in the public database are mostly to prevent abuse, they should be fine for most normal use (the limits scale with the number of users)

iCloud Storage Overview

Categories: iCloud, WWDC 12 0 comments Watch the video

iCloud Storage APIs allow you to store your app’s data in iCloud

System services sync your data automatically even when your app isn’t running

3 different types of storage:

  • key-value storage – simple storage for things like preferences, game state (“it’s so simple, we actually don’t have another session talking about it”)
  • document storage – a filesystem in the cloud scoped for your application, where you can store any kinds of files and folders, synced between devices; ideal for productivity apps like iWork
  • Core Data storage – an extension for Core Data that lets you store Core Data databases in the cloud

What does iCloud handle for you:

  • account setup – users don’t need to create a new account for your service, they already have an iCloud account
  • APIs for your apps integrated into the OSX/iOS SDKs
  • server code you don’t have to write, for things like load balancing, replication, backup and recovery
  • the personnel handling the servers, support etc.

“If you happen to know some friends, who like writing server code that scales to hundreds of millions of users, send them our way – we’re hiring” :D

How it works:

Key-value storage:

Accessed through NSUbiquitousKeyValueStore

Lets you put simple plist-type values into the cloud

It talks to a key-value service running on the device which talks to iCloud on your behalf

Document storage:

Use UIDocument (iOS) / NSDocument (OSX) / UIManagedDocument (Core Data)

You can also use lower level file storage APIs like NSFileCoordination, NSFilePresenter, NSMetadataQuery

All these APIs (also the Core Data iCloud API) talk to OS’s document service which talks to the iCloud for you

You never actually interact with the iCloud servers yourself, you just use these APIs in the SDK and system services

How to enable iCloud in your project:

  • your app needs to be distributed through the App Store [note: no longer true for Mac apps]
  • enable the relevant entitlement: com.apple.developer.ubiquity-kvstore-identifier and/or com.apple.developer.ubiquity-container-identifiers

Working with Key Value Storage:

NSUbiquitousKeyValueStore

Lets you store simple plist values – strings, numbers, booleans, dictionaries and arrays of those

Similar API to UserDefaults

Simple conflict resolution: if two devices independently change or add a value for a given key, the latest change wins

Works even if iCloud isn’t configured – in this case it just acts as local user defaults that don’t sync

Improvements from last year’s initial release:

  • increased capacity – now up to 1024 keys, up to 1 MB per application (not part of the total user quota); it’s fine to have just one key of 1 MB
  • improved responsiveness (allows around 15 requests every 90 seconds)

How to set up:

// get a reference to the store
NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore];

// observe changes
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(kvStoreDidChange:)
               name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
             object:nil];

// ask for any changes since the last launch
[store synchronize];

Making changes:

[store setObject:someObject forKey:@"someKey"];
[store setBool:YES forKey:@"someOtherKey"];

[store synchronize];

It’s recommended to also store another copy of the same data locally, so that you can do conflict resolution manually in case if just keeping the latest change isn’t always the right strategy for your app

Handling notifications about a change:

- (void)kvStoreDidChange:(NSNotification *)notification {
    NSDictionary *userInfo = [notification userInfo];

    // get change reason
    int reason = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
    NSArray *changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];

    // … store values locally, do conflict resolution etc.
}

Working with document storage:

Apart from your app’s container, each app has a separate “ubiquity container” (or iCloud container)

The app can put any files inside that container, and whatever is put there is synced with other devices via iCloud, kind of like Dropbox

When you put a file in the ubiquity container, the file is broken into chunks and the chunks that are new or modified are uploaded to iCloud – so if you only change a few bytes of the file, most of it doesn’t need to be uploaded again

Metadata about the file is uploaded first – info about the file name, type, size etc.

So every device knows about each file that’s uploaded, but it doesn’t necessarily have to download each file – the decision is made independently on the device depending on the platform and settings: OSX usually downloads all files if it has space, iOS only downloads files on demand

Once a file is downloaded to the device, all later changes are also automatically synced

Local peer to peer communication is used if possible – e.g. if you have a file on the Mac, your iPhone will copy it from the Mac instead of the iCloud network

Automatic conflict resolution – if a file is edited on two devices in parallel, the system picks a winner automatically, but your application gets access to both versions and can override it if needed

URL publishing: you can make the current version of the document public and available through a URL which you can share with others (if it’s changed later in the iCloud, the URL still downloads that previous version)

URLs are not permanent, they expire after some time

Detecting an iCloud account:

Unlike key-value storage, using this API requires that the user has an iCloud account

You can check for an “Ubiquity Identity Token” to see if they have an account configured

The token is anonymous so it doesn’t tell you anything about the user, but it will change if the user switches to another account (you also get a notification then)

The token is also unique to your app and to this specific device, so the same app will get a different token from that user on another device

id token = [[NSFileManager defaultManager] ubiquityIdentityToken];
if (token) {
    // cache the token
    // the next time the app launches, compare if it has changed
}

// register for the identity changed notification
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(handleUbiquityIdentityChanged:)
               name:NSUbiquityIdentityDidChangeNotification
             object:nil];

When the account changes, clear any local caches specific to this account and refresh the UI

To access the ubiquity container, you ask for the container URL – note that the container will be created on demand the first time you ask for it; this should ideally not be called on the main thread

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    containerURL = [fileManager URLForUbiquityContainerIdentifier:nil];
});

Types of documents you can store in the document storage:

  • normal files, also symlinks
  • directories of files
  • packages – bundles of files that act as a single document
  • Core Data stores

File extended attributes are also synced

Watch out for filesystem case sensitivity issues – users running on a Mac might have a case-sensitive filesystem

For packages, iCloud updates only the files from a package that have been changed, but it handles updates to the whole package atomically, so you will not get a package in an inconsistent state

Core Data:

Useful for so-called “shoebox style applications”, like iPhoto or iTunes, which work with a single database in app’s own format

The Core Data store remains local and change logs are uploaded to iCloud

Not recommended to use binary and XML stores, because in those cases every change modifies the whole file (only use for small data sets that don’t change often) – use SQLite stores for iCloud sync instead

UIManagedDocument – a subclass of UIDocument for managing Core Data stores that supports syncing them with iCloud

Note: NSPersistentDocument, the AppKit equivalent, does not support iCloud

Designing your document format for iCloud:

Design with network efficiency in mind – don’t write a lot of changes very often

Keep in mind any possible differences between platforms

Plan for future app upgrades – include version number in the format and keep compatibility if possible

Beware of sync loops – when one instance of the app receives a change, merges it and writes back the result, and the other side does the same and triggers a change in the first copy again

“And you have two versions of the app playing ping-pong with the user’s iCloud account”

Avoid making rapid changes to the file, e.g. updating some position tag while the user is scrolling the document

Don’t put the last open date into the document itself, so that opening it doesn’t count as making a change

Use iCloud for user data only: don’t put any caches, temporary files or auto-generated content there

Think about privacy when allowing the user to publish a document: don’t include any sensitive info or things they might not be aware they’re publishing (e.g. undo history) in the publicly accessible view

APIs:

NSFileManager

NSFileCoordinator & NSFilePresenter

NSMetadataQuery – use a live metadata query to be notified of new files and changes before the file contents are downloaded

NSDocument & UIDocument

UIManagedDocument

NSDocument/UIDocument handle most of the integration with iCloud for you:

  • using the ubiquity container
  • coordinating with the OS
  • tracking files and their versions
  • resolving conflicts

Tips:

  • subclass native document types
  • use the default auto-save behavior
  • if the app provides a prepopulated Core Data store on first launch, use a migration instead of copying a packaged store file
  • track documents using NSMetadataQuery
  • if it makes sense for your app, manually control conflict resolution (UIDocumentStateInConflict); avoid user involvement if possible

Tips for debugging:

  • test with multiple devices
  • monitor network traffic
  • use Airplane Mode to create conflicts artificially
  • there will be a configuration profile available that makes the document service log additional messages
  • developer.icloud.com – a web tool that shows you all iCloud storage on your account from various apps

Introducing CloudKit

Categories: CloudKit, iCloud, WWDC 14 0 comments Watch the video

CloudKit lets you write client applications without having to build and host a server part to handle things like database, accounts or push notifications

Usage is free for the developer up to pretty big limits

CloudKit gives you more direct access to iCloud servers

It’s the framework that’s used behind the scenes by iCloud Photo Library and iCloud Drive

Uses the same iCloud account as iCloud documents or key-value storage

Two types of databases:

  • public – accessible to everyone
  • private – private data of a specific user

It’s only a transport technology, it does not deal with local data persistence – you need to decide how you store the data that you load from the cloud

To enable iCloud in your app, set it up in the Capabilities tab in Xcode just like with other iCloud APIs

Containers:

Each app’s data is kept in a separate container (CKContainer)

Containers give you the safety that your app’s data will not be mixed with someone else’s app’s data

A container’s ID needs to be unique in the whole iCloud, so use reverse-domain style identifiers

By default each app has one container of its own, but apps can additionally use shared containers

Containers are managed by the developer through the WWDR portal

Databases:

A container contains one shared public database for everyone, and separate private databases for each user

An app running on the device has access to one public and one private database (CKDatabase)

The database is the initial entry point to CloudKit (from a container)

CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];
CKDatabase *privateDatabase = [[CKContainer defaultContainer] privateCloudDatabase];

Private database:

  • requires a logged in iCloud account
  • data stored counts against the user’s iCloud account quota
  • default permission for data is user readable
  • the data your users store in your app’s CloudKit container is *not* accessible to you

Public database:

  • can be accessed anonymously even if the user isn’t logged in
  • data stored counts against the developer’s app quota
  • default permission for data is world readable
  • permissions can be customized using iCloud Dashboard Roles

Records:

A record (CKRecord) is a single “object” in the CloudKit database, essentially a list of key-value pairs

Records have a Record Type (~ table name)

There is no defined up front schema, you can just save a record of any type with any keys and the schema will be updated based on that

→ note: this only works in development, the schema is fixed in production, see "Advanced CloudKit"

Records can have metadata: who created it & modified it and when, also includes a “change tag” (version id) – for determining if two sides have the same version of a record

Record values can be: strings, numbers, dates, NSData, CLLocation, CKReference, CKAsset, arrays of any of these

- (instancetype)initWithRecordType:(NSString *)recordType;
- (id)objectForKey:(NSString*)key;
- (void)setObject:(id)object forKey:(NSString *)key;
- (NSArray *)allKeys;

Subscripts also work:

CKRecord *party = [[CKRecord alloc] initWithRecordType:@"Party"];
party[@"start"] = [NSDate date];

Record zones:

Records are grouped within a database inside “zones” (CKRecordZoneID)

The public database has one zone, the private database has one default zone, but it can have additional custom zones

Record identifiers:

Record identifier (CKRecordID) is a tuple grouping: a “record name” + zone ID

You can provide a recordID when creating a record instance

If you don’t provide a recordID, a random UUID will be assigned

References:

A reference (CKReference) is a pointer from one record to another, as an id of the “parent” record contained in a child record’s field

References allow you to do cascade deletes, deleting child records when parent is deleted

You can create a reference from a CKRecord object or from a CKRecordID if you know the ID but don’t have the object in memory

Assets:

An asset (CKAsset) is an unstructured piece of data, basically a binary file

Assets are downloaded and uploaded from/to files on disk, not from memory

An asset is always owned by a record, and is deleted when the record is deleted

Transport of assets is optimized so that only the minimal amount of data is transferred

APIs:

There are two different APIs for managing CloudKit data: “operational API” and “convenience API”

The operational API has every possible operation you might need, the convenience API is more convenient

Start with the convenience API, use operational API for tweaking and overriding options if needed

CloudKit APIs for saving/fetching data are asynchronous – there is no SDK-managed local data, everything needs to go over the network unless you manually cache it

In CloudKit it’s absolutely necessary to properly handle error cases – every network call can fail and your app needs to be prepared for this

Convenience API:

[publicDatabase saveRecord:obj completionHandler: { … }];
[publicDatabase retchRecordWithID:recordID completionHandler: { … }];

Queries:

For any large database, or the shared public database, you shouldn’t try to keep a copy of the whole database on disk and sync all of it, but instead fetch what you need on demand – for this you can use queries

A query (CKQuery) allows you to fetch a list of records matching some conditions

Query can specify a RecordType, NSPredicate and optionally NSSortDescriptors

A subset of NSPredicate language is supported, if something is not supported you’ll get an exception

Predicates such as “equal”, “greater than”, “distance to location”, string tokenizing, and OR / AND are supported

[publicDatabase performQuery:query inZoneWithID:nil completionHandler: { … }];

Subscriptions:

If you repeatedly run the same query, polling for the same data, you can ask the server to run the query for you and notify you immediately when a new record is added

You do that by creating a subscription (CKSubscription)

A subscription includes: RecordType, NSPredicate and push configuration (CKNotificationInfo)

Your app is notified of changes through a push notification with some additional data

CKSubscription *subscription =
  [[CKSubscription alloc] initWithRecordType:@"Party"
                                   predicate:predicate
                                     options:CKSubscriptionOptionsFiresOnRecordCreation];

[publicDatabase saveSubscription:subscription completionHandler: { … }];

Pushes are handled through the usual push API:

-(void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo;

Build a CKNotification object from the user info:

CKNotification *cloudKitNotification =
    [CKNotification notificationFromRemoteNotificationDictionary:userInfo];

NSString *alertBody = cloudKitNotification.alertBody;

if (cloudKitNotification.notificationType == CKNotificationTypeQuery) {
    CKQueryNotification *queryNotification = cloudKitNotification;
    CKRecordID *recordID = [queryNotification recordID];
}

Handling user accounts:

Your application does not get direct access to any user identifiers like iCloud email address

Instead, in each container each user is represented as a unique ID within that container that doesn’t change unless the user switches to another account

The same user will have a different ID in a different CloudKit container

The ID is an instance of CKRecordID

[[CKContainer defaultContainer] fetchUserRecordIDWithCompletionHandler: { … }];

Each user has a user record representing them (which is almost like any other record) with their user id and record type = CKRecordTypeUserRecord, one in the private database, and another with the same ID in the public database

You can set and read any key-value data on this record like on other records

However, these records aren’t created by you and can’t be queried to get a list of users

[publicDatabase fetchRecordWithID:userRecordID completionHandler: { … }];

User discovery:

You can ask the user to allow you to make them discoverable by other users (they get a request popup)

If they agree, they can be looked up by user ID, specific email, or by fetching a list of all users matching your user’s contacts from the address book (this doesn’t give your app access to the address book itself, just a list of matching users)

You get back record IDs, first & last names of users, but no emails

[defaultContainer discoverAllContactUserInfosWithCompletionHandler: { … }];

Returns an array of CKDiscoveredUserInfo objects with properties userRecordID, firstName, lastName

When to use CloudKit vs. other APIs?

CloudKit doesn’t replace or deprecate any existing iCloud APIs [yet ;P], it’s just an additional tool

Key-value store:

  • asynchronous, small amounts of data
  • mostly for application preferences

iCloud Drive:

  • works on files and folders
  • on OSX it makes a full offline cache of the drive
  • good for document-centric apps

iCloud Core Data:

  • built on top of iCloud Drive
  • good for keeping private, structured data (custom databases) in sync
  • note: the whole data set is downloaded to each device

CloudKit:

  • good for sharing public data between users, both structured data and large files
  • good for large data sets where not every device needs to have a copy of the whole database
  • for attaching some data to the user’s identity and sharing info between users that know each other
  • more low-level, your app is in control of when any information is downloaded or uploaded to the iCloud servers, and has responsibility for handling sync

SwiftUI on watchOS

Categories: SwiftUI, WatchKit, WWDC 19 0 comments Watch the video

SwiftUI offers apps some capabilities that were not possible before, e.g. swipe to delete and reordering in lists, or an easy way to do custom graphics and animations

Fully integrated with WatchKit, both ways

SwiftUI allows you to use the same code on all platforms

However, Apple Watch is a very special platform; a watchOS app should not be just a tiny version of the iOS app, it should be specifically designed for the Watch by only picking the right elements of the experience

Building an Apple Watch app is building the whole experience, not just the main app, but also complications, notifications, Siri interface – depending on the app

Use subclasses of WKHostingController to embed SwiftUI views in an interface controller that WatchKit can use

.font(.system(.headline, design: .rounded)) – uses a rounded version of San Francisco

.listRowPlatterColor(topic.color) – sets the color of the list cell background

.listStyle(.carousel) – a list design which centers the currently focused item on the screen

.onMove { } – enables drag to reorder

.onDelete { } – enables swipe to delete

For notification UI, inherit from WKUserNotificationHostingController

When a notification is received (didReceive(_:)), the view body is automatically invalidated and reloaded

Using digital crown:

Fluent scrolling between the beginning and the end:

.digitalCrownRotation($binding, from:, through:)

→ lets you create some kind of custom scrollable container

Discrete values:

.digitalCrownRotation($binding, from:, through:, by:)

→ for building interfaces where e.g. some value moves up or down by 1 when scrolling

Going around in a circle:

.digitalCrownRotation($binding, from:, through:, by:, sensitivity:, isContinuous: true)

→ sensitivity says how fast it rotates

.focusable(true) – lets the user switch focus between elements; digital crown events go to the focused item

Explore logging in Swift

Categories: Foundation, Logging, WWDC 20 0 comments Watch the video

Xcode 12 introduces new APIs for the unified logging system:

import os

let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")
logger.log("Started a task")

It's now much easier to pass arguments to logs using new Swift interpolation than the old os_log API with C-style formatters:

logger.log("Started a task \(taskId)")

Like in os_log, it does not generate the resulting formatted string at the moment of logging, but stores an optimized representation and defers formatting to a later moment

You can pass any values like numeric types, ObjC types that implement description and any type conforming to CustomStringConvertible

Non-numeric data is private by default:

logger.log("Paid with bank account \(accountNr)")
// -> Paid with bank account <private>

To actually display the values, use the privacy: parameter:

logger.log("Ordered smoothie \(smoothieName, privacy: .public)")

You can also log private data in an obfuscated form, but hashed in such a way that each value gets its unique hash which you can track through a chain of logs:

logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
// -> Paid with bank account <mask.hash:'CSvWylJ63...'>

Collecting recent logs from a connected device to a file:

log collect --device --start "2020-06-22 9:41" --output fruta.logarchive

Log levels:

  • debug – useful only during debugging
  • info – helpful but not essential for troubleshooting
  • notice (default) – essential for troubleshooting
  • error – expected errors
  • fault – unexpected errors, assumptions that weren’t true, potential bugs

The Logger has a separate method for each log level:

logger.debug(…)
logger.error(…)
// etc.

Take into account that not all logs are persisted after the app finishes execution, some can only be live streamed

debug  ⭢  not persisted by default

info  ⭢  saved to memory, only persisted if collected using log collect before they disappear

notice  ⭢  persisted up to a storage limit, older messages are purged after some time

error, fault  ⭢  persisted for a longer time than notice messages (typically a few days)

The level also affects logging performance: debug logs are the fastest, error and fault are the slowest

It’s safe to call even some slower code within log messages that will not be logged – the code isn’t run unless the log is enabled:

logger.debug("\(slowFunction(data))")

Logger provides a number of built-in formatters that let you customize how an interpolated value is displayed:

logger.log("\(cardId, align: .left(columns: 5))")
logger.log("\(seconds, format: .fixed(precision: 2))")
logger.log("\(data, format: .hex, align: .right(columns: 4))")

+ others like decimal, exponential, octal

Existing os_log(…) function also accepts the new interpolating parameters (when running on latest OS versions)

Measuring Performance Using Logging

Categories: Foundation, Logging, Performance, WWDC 18 0 comments Watch the video

Signposts – a new feature of the os_log API

Useful for debugging performance issues

Integrated with Instruments, which can visualize activity over time using signposts

Signposts allow you to mark the beginning and end of a piece of work and mark it with some kind of label

import os.signpost

let refreshLog = OSLog(subsystem: "…", category: "…")

os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
// …do actual work…
os_signpost(.end, log: refreshLog, name: "Fetch Asset")

Ranges of signposts with different names can overlap – e.g. you can have one signpost covering the whole process and smaller signposts covering specific single tasks that it consists of:

os_signpost(.begin, log: log, name: "Load Data")

os_signpost(.begin, log: log, name: "Fetch Asset")
// …
os_signpost(.end, log: log, name: "Fetch Asset")

os_signpost(.begin, log: log, name: "Parse JSON")
// …
os_signpost(.end, log: log, name: "Parse JSON")

os_signpost(.end, log: log, name: "Load Data")

If you run multiple tasks of the same kind, to let the system differentiate between them and know which begin matches which end, you can add a “signpost ID”:

os_signpost(.begin, log: log, name: "Load Data")

for asset in assets {
    let spid = OSSignpostID(log: log)

    os_signpost(.begin, log: refreshLog, name: "Fetch Asset", signpostID: spid)
    // …do actual work…
    os_signpost(.end, log: refreshLog, name: "Fetch Asset", signpostID: spid)
}

os_signpost(.end, log: log, name: "Load Data")

You can also pass your model object when generating the signpost ID – then it will always use the same signpost ID for the same object, and you don't have to store the signpost object, just your model:

let spid = OSSignpostID(log: log, object: asset)

There is also an option of passing an additional string argument to begin/end to provide some context (e.g. to differentiate between different possible ways to finish an activity, like success/failure)

The string also accepts format arguments like os_log:

os_signpost(.begin, log: log, name: "Compute Physics",
    "Calculating %{public}s: %d %d %d %d", description, x1, y2, x2, y2)

Apart from marking the beginning and end, you can also mark specific points in time during the process using the .event signpost type:

os_signpost(.event, log: log, name: "Fetch Asset",
    "Received chunk of data, size %d", size)

Signposts are very optimized internally, they’re built to minimize the time spent when logging, same as the whole os_log API

This means you can emit a lot of signposts even in a very tight window, when investigating a performance bottleneck

If you still really want to enable or disable some signpost logs based on some conditions, you can swap your logger object with OSLog.disabled:

let refreshLog: OSLog

if ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_REFRESH") {
    refreshLog = OSLog(…)
} else {
    refreshLog = .disabled
}

To conditionally disable some expensive code that is only useful for debugging, you can check the signpostsEnabled property:

if refreshLog.signpostsEnabled {
    let information = collectInfo()
    os_signpost(…, information)
}

All APIs are also available in C & ObjC:

#include <os/signpost.h>

os_signpost_interval_begin()
os_signpost_interval_end()
os_signpost_event_emit()
os_signpost_id_t
OS_LOG_DISABLED

Use the formatter %{xcode:size-in-bytes}u to let Xcode & Instruments know that the logged value is a byte size of some data

→ This is one of so called “Engineering types” – find more in the Instruments Help menu, in the Instruments developer guide

Instruments:

Use the “os_signpost” instrument to profile using signposts

After recording some data, you can see the signpost names in the sidebar on the left and signpost ranges with the optional begin/end comments marked on the chart

In the bottom pane you can see count and duration statistics, grouped by category, signpost, id and comment

Clicking the arrow button next to a specific message row shows you a list of all instances of this specific message (selecting them highlights them on the timeline)

For metadata like logged byte sizes of downloads, you can choose “Summary: Metadata Statistics” to see total/min/max/avg of each type of value

Live streaming signpost logs to Instruments (“Immediate mode”) adds some overhead, so if you want to avoid that while debugging some performance-critical code, click and hold the Record button to access recording options and change mode to “Last n seconds”

Points of interest:

Points of interest is a special log category of OSLog

It’s meant for logging important actions taken by the user, like opening some specific screen

Normal os_log logs and signpost logs logged to this category appear in a special separate Instruments timeline, which lets you visualize what was happening in the app at the moment when something happened on other charts like CPU usage

Custom Instruments packages:

You can now build your custom Instruments packages, defined as an XML file in a separate target, which appear as a new kind of template when starting Instruments

This lets you process and present collected signpost data in a different way that makes sense for the specific problem you’re analyzing