iOS、CloudKit - 我的应用程序启动时是否需要进行提取?

iOS, CloudKit - do I need to do a fetch when my app starts?

我正在为 iCloud 更改通知设置注册。

假设一个新设备被添加到icloud帐户,我只是想知道该设备将如何获取私人数据库记录。

我需要一次性查询吗?

我希望在所有其他时间都能使用通知。

让我们从订阅通知的一些相关特征开始:

首先: 订阅通知特定于用户 + 设备对。如果我在我的 phone 上安装你的应用程序,我就会开始收到通知。在我也安装该应用程序之前,我不会在另一台设备上收到通知。

其次:通知不可靠。苹果文档很清楚,他们不保证交付。当您收到通知时,之前可能有多个通知。因此,Apple 提供了两种机制来跟踪您看到的通知:

  1. Read/unread status: 您可以将通知标记为已读。 Apple 的文档在这方面的实际作用自相矛盾。 This page

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.

然而,事实并非如此。提取操作显然 returns 已读和未读通知。 WWDC 2014 视频 231(高级 Cloudkit)与文档页面相矛盾,解释说未读令牌总是 returned 以及已读令牌,因此多个设备可以同步。该视频提供了一个具体示例,展示了此行为的好处。此行为也记录在 SO 上:CKFetchNotificationChangesOperation returning old notifications

  1. 更改令牌:每个提取操作都会return一个您可以缓存的更改令牌。如果您将令牌传递给提取,则提取将从该点开始仅 return 个令牌,无论是已读还是未读。

乍一看,Apple 似乎正在提供您想要的行为:在一台设备上安装应用程序,开始处理通知,在第二台设备上安装应用程序,并获取所有这些先前的通知以便赶上。

不幸的是,正如我在 CKFetchNotificationChangesOperation: why are READ notifications all nil? 中记录的那样,每当我获取通知时,之前标记为 "read" 的通知都没有内容。已读通知中的所有信息都丢失了。

在我的场景中,我选择了:

  1. 始终在启动时获取最新记录
  2. 使用之前保存的更改令牌(如果存在)获取通知
  3. 处理新通知
  4. 将通知标记为已读
  5. 保存最新的更改令牌以供下次提取使用

对于您的场景,您可以尝试:

  1. 使用之前保存的更改令牌(如果存在)获取通知
  2. 处理通知(不要将它们标记为已读)
  3. 保存最新的更改令牌以供下次提取使用

您的第一台设备将在每次后续提取时忽略旧通知,因为您是从更改令牌点开始每次提取。您的第二个设备将在第一次执行时以零更改令牌开始,从而接收所有旧通知。

提醒一句:尽管前面提到的 WWDC 视频明确表示 Apple 会保留所有旧通知,但我没有找到说明他们保留这些信息多长时间的文档。可能是永远,也可能不是。

更新了通知获取示例

以下是我获取通知、将它们标记为已读以及缓存更改令牌的方式:

@property CKServerChangeToken *notificationServerChangeToken;

然后...

-(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];
}

要从用户默认设置和检索缓存的更改令牌,我使用以下两个函数:

-(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.
}

和...

-(void)getNotificationServerChangeToken
{
    NSUserDefaults *userSettings = [NSUserDefaults standardUserDefaults];
    NSData *encodedServerChangeToken = [userSettings objectForKey:UD_KEY_NOTIFICATION_TOKEN_CKSERVERCHANGETOKEN_PROD];
    _notificationServerChangeToken = [NSKeyedUnarchiver unarchiveObjectWithData:encodedServerChangeToken];    
}