Let's start with some relevant characteristics of subscription notifications:
First: Subscription Notifications are specific to a user + device pair. If I install you app on my phone, I start getting notifications. I won't get the notifications on another device until I install the app there, too.
Second: Notifications are unreliable. Apple docs are quite clear that they do not guarantee delivery. When you receive a notification, there could have been several prior notifications. Thus, Apple offer's two mechanisms to track which notifications you've seen:
- Read/unread status: you can mark notifs as read. Apple's docs contradict themselves about what this actually does. This page says
If you mark one or more notifications as read using a CKMarkNotificationsReadOperation object, those notifications are not returned, even if you specify nil for previousServerChangeToken.
However, this isn't true. The fetch operation clearly returns both read and unread notifications. WWDC 2014 Video 231 (Advanced Cloudkit) contradicts the documentation page, explaining that unread tokens are always returned as well as read tokens so multiple devices can sync up. The video gives a specific example that shows the benefits of this behavior. This behavior is also documented on SO: CKFetchNotificationChangesOperation returning old notifications
- change token: each fetch operation will return a change token that you can cache. If you pass the token to a fetch, the fetch will only return tokens from that point, whether read or unread.
At first glance, it would seem that Apple is providing for the behavior you want: install the app on one device, start processing notifications, install the app on a second device, and fetch all those prior notifications in order to catch up.
Unfortunately, as I've documented in CKFetchNotificationChangesOperation: why are READ notifications all nil?, any time I fetch notifications, the ones previously marked as "read" all have nil contents. All the info in the read notifications is lost.
In my scenario, I chose to:
- Always fetch the latest record(s) at startup
- Fetch notifications using the previously saved change token (if it exists)
- Process the new notifications
- Mark the notifications as read
- save the latest change token for use on the next fetch
For your scenario, you could try:
- Fetch notifications using the previously saved change token (if it exists)
- process the notifications (DO NOT MARK THEM AS READ)
- save the latest change token for use on the next fetch
Your first device will ignore old notifications on each subsequent fetch because you're starting each fetch from the change token point. Your second device will start with a nil change token on the first execution, and thus pick up all of the old notifications.
One word of caution: even though aforementioned WWDC video clearly says Apple keeps all the old notifications, I have found no documentation that says how long they hold this info. It may be forever, it may not.
updated with notification fetch example
Here's how I'm fetching notifications, marking them read, and caching the change token:
@property CKServerChangeToken *notificationServerChangeToken;
Then...
-(void)checkForUnreadNotifications
{
//check for unread cloudkit messages
CKFetchNotificationChangesOperation *op = [[CKFetchNotificationChangesOperation alloc] initWithPreviousServerChangeToken:_notificationServerChangeToken];
op.notificationChangedBlock = ^(CKNotification *notification)
{
//this fires for each received notification. Take action as needed.
};
//maintain a pointer to the op. We will need to look at a property on the
//op from within the completion block. Use __weak to prevent retain problems
__weak CKFetchNotificationChangesOperation *operationLocal = op;
op.fetchNotificationChangesCompletionBlock = ^(CKServerChangeToken *newServerChangeToken, NSError *opError)
{
//this fires once, at the end, after all notifications have been returned.
//this is where I mark the notifications as read, for example. I've
//omitted that step because it probably doesn't fit your scenario.
//update the change token so we know where we left off
[self setNotificationServerChangeToken:newServerChangeToken];
if (operationLocal.moreComing)
{
//more notifications are waiting, recursively keep reading
[self checkForUnreadNotifications];
return;
}
};
[[CKContainer defaultContainer] addOperation:op];
}
To set and retrieve the cached change token from the user defaults, I use the following two functions:
-(void)setNotificationServerChangeToken:(CKServerChangeToken *)newServerChangeToken
{
//update the change token so we know where we left off
_notificationServerChangeToken = newServerChangeToken;
NSData *encodedServerChangeToken = [NSKeyedArchiver archivedDataWithRootObject:newServerChangeToken];
NSUserDefaults *userSettings = [NSUserDefaults standardUserDefaults];
[userSettings setObject:encodedServerChangeToken forKey:UD_KEY_NOTIFICATION_TOKEN_CKSERVERCHANGETOKEN_PROD];
//Note, the development and production cloudkit environments have separate change tokens. Depending on your needs, you may need to save both.
}
and...
-(void)getNotificationServerChangeToken
{
NSUserDefaults *userSettings = [NSUserDefaults standardUserDefaults];
NSData *encodedServerChangeToken = [userSettings objectForKey:UD_KEY_NOTIFICATION_TOKEN_CKSERVERCHANGETOKEN_PROD];
_notificationServerChangeToken = [NSKeyedUnarchiver unarchiveObjectWithData:encodedServerChangeToken];
}