Subscriptions & Notifications

CloudKit can send your app silent notifications in response to changes in the database. You can choose to be notified when records are created, updated, or deleted. These are known as 'subscriptions' and we're going to show you how to set them up in this tutorial.

Setup

Add the APS capability

Subscriptions require the push notification service. So in order to get this all working, we need to make sure that our app has that service enabled in its provisioning profile. Log into the apple developer portal. Click on Certificates, Identifiers & Profiles. Select identifiers from the panel on the side. Find and click your apps bundle identifier to bring up its capabilities. Scroll down and make sure that you have the 'Push Notifications' capability checked.

Create an SSL certificate

Even if you've enabled push notifications, they won't work without an SSL certificate. Click the 'edit' button next to the Push Notification capability. You'll see options for creating development and production SSL certificates. For local development, we will be using the development SSL certificate but there's no harm in setting up both if you want to do that now. Click the "development" button, on the next screen. You'll need to generate a signing request from the Keychain app on your computer and upload it to this page.

Next, download the certificate to your computer. Open the certificate in order to add it to your computer's Keychain. This is an important step, you won't be able to receive notifications unless you do this.

MacOs

XCode automatically repairs any provisioning profiles that don't match their app's capabilities. But since MacOS doesn't build to an XCode project, will need to regenerate your provisioning profile after this step, and replace the existing one in your project

TVOS

TVOS builds will need to perform the additional step of copying the SSL certificate to Apple TV. Apple provides no way to do this from Xcode. Sadly, this step requires that you upload your development certificate to a web-accessible URL and download it from the Apple TV's system settings. You can read more about how to do that here:Technical Q&A QA1948 HTTPS and Test Servers. See the section entitled: 'tvOS Device and Simulator'

Edit Plugin Build Settings

Head back to your unity project. Open the plugin's build settings and make sure that 'Enable CloudKit Notifications' is checked. This ensures that the appropriate entitlements and capabilities are added to your Xcode project or your macOS application when you build. Also, make sure that the 'BackgroundModes' selection includes 'Remote Notifications'. This setting is only required by iOS and TVOS. macOS does not use background modes.

Let's Get Coding

With the setup out of the way we can get coding. If you want to jump ahead and just see some working code check out 'Example9_Subscriptions.cs' from the provided samples.

There are two basic steps here. First, we create the subscription and second we register for remote notifications. Creating subscriptions only needs to occur once for a given user. But we'll need to register for remote notifications each time we launch the app.

Create a subscription

Subscriptions only need to be created once for a given user. In fact, attempting to add the same subscriptions more than once will result in the subscription request failing with an error. So, when you create your app, you may want to store some information about which subscriptions you've already created for the user. In lieu of that, however, were just going to start by checking if the current user has any active CloudKit subscriptions and add one if they have none.

This step will work even if you're app is missing the appropriate push notificatios entilements, but you wont be able to receive notifications without it.

This step will work even if you're app is missing the appropriate push notification entitlements, but you won't be able to receive notifications without it. To check if the current user has any subscriptions. Make a call to "FetchAllSubscriptionsWithCompletionHandler" on the database instance.

  
var database = CKContainer.DefaultContainer().PrivateCloudDatabase;
database.FetchAllSubscriptionsWithCompletionHandler(OnSubscriptionsFetched);
  


The function takes as it's argument a callback which is invoked when the request is complete. The callback has two arguments, the first is an array of all the subscriptions the user has active in the database. The second is an error, which will be non-null if something went wrong with the request. Always check the error before attempting to do anything with the subscriptions

  
  private void OnSubscriptionsFetched(CKSubscription[] subscriptions, NSError error)
  {
      if(error != null)
      {
          Debug.LogError(error.LocalizedDescription);
          return;
      }

      if(subscriptions.Length > 0)
      {
          var subscriptionIds = subscriptions.Select(sub => sub.SubscriptionID.ToString()).ToArray();
          Debug.LogFormat("You have the following subscriptions: {0}",
              string.Join(",", subscriptionIds));
          return;
      }
      else
      {
          // User has no active subscriptions. Create one
          StartNewSubscription();
      }
  }
  
  

Assuming the user has no active subscriptions, we're going to create one. One of the requirements for a subscription is that it be attached to a specific RecordType in the database. If you don't have this record type setup in the database schema yet, do so now. Without it in place, the request to create a new subscription will fail.

    
    private void CreateSubscription()
    {
        var database = CKContainer.DefaultContainer().PrivateCloudDatabase;
        var predicate = NSPredicate.PredicateWithValue(true);

        var notificationInfo = new CKNotificationInfo();

        var querySubscription = new CKQuerySubscription(
            recordType,
            predicate,
            CKQuerySubscriptionOptions.FiresOnRecordCreation |
            CKQuerySubscriptionOptions.FiresOnRecordDeletion |
            CKQuerySubscriptionOptions.FiresOnRecordUpdate);

        querySubscription.NotificationInfo = notificationInfo;

        database.SaveSubscription(querySubscription, OnSubscriptionSaved);
    }
    
  

There are a few steps to setting up a subscription. In this tutorial, we're creating a subscription of the type CKQuerySubscription which takes a predicate. A predicate in objective-c is a string expression, which evaluates to true or false when run on a record. You can do some pretty advanced things with them, for instance, you can create a predicate that only returns true if the name of the record contains the string "cats". But for the purposes of this tutorial, we're just going to use a predicate that always returns true. This means that our notification will always fire whenever the enabling condition is met.

The next requirement for the subscription is a notification info object. These contain a few settings such as whether to badge the application in the launcher for unhandled notifications, and whether to play a sound. Since CloudKit notifications are silent (not displayed to the use) they should not be badged, or play a sound. Even if you don't adjust the default notification settings, you must set one.

The constructor for the subscription will take three arguments. We've covered the first two things, a string containing a "RecordType" which must match a record type in the database schema and the predicate we created. The third is a flag, which sets under what conditions the notification will fire. You can choose to only receive a notification when records are saved, updated, or deleted (or any combination of those things)

Once your subscription object is created, fire it off in a request by calling 'SaveSubscription' on your database instance. The SaveSubscription method takes as a second parameter a callback function which will fire when the request is complete.

Registering for Remote Notifications

Once we have our subscription created. The next step is to register for remote notifications. Unlike the subscription itself, this needs to be done each time we launch the application. The first step is to request a notification token by calling the static method RequestNotificationToken on the ICloudNotifications class. It takes as it's only parameter a callback function which will indicate if the token was successfully retrieved or there was an error

Technical Note

Calling this function will cause the plugin to swizzle in a few functions to replace the default behavior of the remote notification handlers in the app delegate. Other plugins that handle remote notifications may interfere with the correct behavior of this one if they have overwritten the default app delegate class.

Call this function early on in your application's lifecycle, so that you do not miss any potential notifications


  ICloudNotifications.RequestNotificationToken((tokenBytes, error) => {
      if(error != null)
      {
          Debug.LogError("Failed to get push notification token: " + error.LocalizedDescription);
      }
      else
      {
          Debug.LogError("Got push notification token");
      }
  });
  

Next, we set a handler for remote notifications by calling the static method 'SetRemoteNotificationHandler' on the class ICloudNotifications. Provide it will a callback to be invoked when a new CloudKit notification comes in.

  
  private void OnQueryNotification(CKNotification notification)
  {
      Debug.Log(string.Format("Recieved notification: {0} {1}", notification.NotificationID, notification.NotificationType));

      if (notification is CKQueryNotification queryNotification)
      {
          Debug.Log("Notification Reason: " + queryNotification.QueryNotificationReason);
          Debug.Log("RecordID: " + queryNotification.RecordID.RecordName);
      }
  }
  
  

This callback will be invoked for every notification we recieve, for all subscriptions we've created for this user. It's only parameter is a CKNotification instance - which must be upcast to the appropriate concrete type before we can really do anything with it. In this case, we check if it's a CKQueryNotification. The CKQueryNotification object will contain a RecordID property which is the Id of the record that was just modified on the database