首页 > 解决方案 > 安排并发症更新

问题描述

我在 Apple Watch 上有一个自定义复杂功能,我试图每小时更新一次。每小时它应该对一个 API 端点执行 ping 操作,如果数据与上次检查相比发生了变化,则应该更新并发症。

这是我目前拥有的似乎只能在蓝月亮中工作一次的东西。当它确实起作用时,它确实会 ping 我的服务器并更新并发症。看来 WatchOS 只是没有每小时调用一次我的计划任务。我缺少更好的标准做法吗?

@implementation ExtensionDelegate

- (void)applicationDidFinishLaunching {
    // Perform any final initialization of your application.
    [SessionManager sharedManager];

    [self scheduleHourlyUpdate];
}

- (void) scheduleHourlyUpdate {
    NSDate *nextHour = [[NSDate date] dateByAddingTimeInterval:(60 * 60)];
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar]
    components: NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond fromDate:nextHour];

    [[WKExtension sharedExtension] scheduleBackgroundRefreshWithPreferredDate:nextHour userInfo:nil scheduledCompletion:^(NSError * _Nullable error) {
        // schedule another one in the next hour
        if (error != nil)
            NSLog(@"Error while scheduling background refresh task: %@", error.localizedDescription);
    }];
}

- (void)handleBackgroundTasks:(NSSet<WKRefreshBackgroundTask *> *)backgroundTasks {
    // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
    for (WKRefreshBackgroundTask * task in backgroundTasks) {
        // Check the Class of each task to decide how to process it
        if ([task isKindOfClass:[WKApplicationRefreshBackgroundTask class]]) {
            // Be sure to complete the background task once you’re done.
            WKApplicationRefreshBackgroundTask *backgroundTask = (WKApplicationRefreshBackgroundTask*)task;
            [backgroundTask setTaskCompletedWithSnapshot:NO];
            [self updateComplicationServer];
        } else if ([task isKindOfClass:[WKSnapshotRefreshBackgroundTask class]]) {
            // Snapshot tasks have a unique completion call, make sure to set your expiration date
            WKSnapshotRefreshBackgroundTask *snapshotTask = (WKSnapshotRefreshBackgroundTask*)task;
            [snapshotTask setTaskCompletedWithDefaultStateRestored:YES estimatedSnapshotExpiration:[NSDate distantFuture] userInfo:nil];
        } else if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) {
            // Be sure to complete the background task once you’re done.
            WKWatchConnectivityRefreshBackgroundTask *backgroundTask = (WKWatchConnectivityRefreshBackgroundTask*)task;
            [backgroundTask setTaskCompletedWithSnapshot:NO];
        } else if ([task isKindOfClass:[WKURLSessionRefreshBackgroundTask class]]) {
            // Be sure to complete the background task once you’re done.
            WKURLSessionRefreshBackgroundTask *backgroundTask = (WKURLSessionRefreshBackgroundTask*)task;
            [backgroundTask setTaskCompletedWithSnapshot:NO];
        } else if ([task isKindOfClass:[WKRelevantShortcutRefreshBackgroundTask class]]) {
            // Be sure to complete the relevant-shortcut task once you’re done.
            WKRelevantShortcutRefreshBackgroundTask *relevantShortcutTask = (WKRelevantShortcutRefreshBackgroundTask*)task;
            [relevantShortcutTask setTaskCompletedWithSnapshot:NO];
        } else if ([task isKindOfClass:[WKIntentDidRunRefreshBackgroundTask class]]) {
            // Be sure to complete the intent-did-run task once you’re done.
            WKIntentDidRunRefreshBackgroundTask *intentDidRunTask = (WKIntentDidRunRefreshBackgroundTask*)task;
            [intentDidRunTask setTaskCompletedWithSnapshot:NO];
        } else {
            // make sure to complete unhandled task types
            [task setTaskCompletedWithSnapshot:NO];
        }
    }
}

- (void)updateComplicationServer {    
    [self scheduleHourlyUpdate];

    NSString *nsLogin = [NSUserDefaults.standardUserDefaults objectForKey:@"loginDTO"];

    if (nsLogin != nil)
    {
        NSDateComponents *dateComponents = [[NSCalendar currentCalendar]
        components: NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay fromDate:[NSDate date]];

        LoginDTO *login = new LoginDTO([nsLogin cStringUsingEncoding:NSUTF8StringEncoding]);

        NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.myurl.com/Api/Watch/Complication"]];
        [req setHTTPMethod:@"GET"];

        // Set headers
        [req addValue:[NSString stringWithUTF8String:login->GetApiKey()] forHTTPHeaderField:@"MySessionKey"];
        [req addValue:[NSString stringWithFormat:@"%d,%d,%d", dateComponents.year, dateComponents.month, dateComponents.day] forHTTPHeaderField:@"FetchDate"];

        [req addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDataTask *task = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
        {
            // Call is complete and data has been received
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
            if (httpResponse.statusCode == 200)
            {
                NSString* nsJson = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

                NSString *prevJson = [NSUserDefaults.standardUserDefaults objectForKey:@"previousComplicationJson"];

                if (prevComplicationJson != nil)
                {
                    if ([prevComplicationJson isEqualToString:nsJson])
                        return; // Nothing changed, so don't update the UI.
                }

                // Update the dictionary
                [NSUserDefaults.standardUserDefaults setObject:nsJson forKey:@"previousComplicationJson"];

                CLKComplicationServer *server = [CLKComplicationServer sharedInstance];
                for (int i = 0; i < server.activeComplications.count; i++)
                    [server reloadTimelineForComplication:server.activeComplications[i]];
            }
        }];

        [task resume];

        delete login;
    }
}

标签: watchkitapple-watchapple-watch-complication

解决方案


watchOS 后台任务的实现和调试异常繁琐,但根据其他人讨论过的 Apple 文档和实现,我认为这是最佳实践。我在这里看到了几个问题。

首先,来自WKRefreshBackgroundTask 文档

一旦所有后台任务完成,系统就会暂停扩展。

调用setTaskCompletedWithSnapshot任务向系统表明您已经完成了您需要做的所有工作,因此它将暂停您的应用程序。您的updateComplicationServer方法可能永远没有机会运行,因为系统过早地暂停了您的扩展。

更重要的是,要在后台更新期间发出 URL 请求,您需要使用后台 URL 会话。WKRefreshBackgroundTask 文档中概述的示例过程显示了设置它的最佳实践。简而言之:

  1. WKExtension您可以使用'来安排后台刷新scheduleBackgroundRefresh,就像您正在做的那样。
  2. 系统将在您首选日期之后的某个时间(由系统自行决定)唤醒您的分机,并带有WKRefreshBackgroundTask.
  3. 在您的扩展委托的handle方法中,检查WKApplicationRefreshBackgroundTask; 而不是在这里执行请求URLSessionDataTask,您需要安排一个后台URL 会话,以便系统可以暂停您的扩展程序并代表您执行请求。有关如何设置后台会话的详细信息,请参阅WKURLSessionRefreshBackgroundTask文档。
  4. 系统将在单独的进程中执行您的 URL 请求,并在完成后再次唤醒您的扩展程序。它将handle像以前一样调用您的扩展委托的方法,这次使用WKURLSessionRefreshBackgroundTask. 在这里,您需要做两件事:

    • 将后台任务保存在扩展委托的实例变量中。我们还不想将其设置为完成,但我们需要保留它以便稍后在 URL 请求完成时设置完成。
    • 使用后台任务创建另一个后台 URL 会话sessionIdentifier,并使用您的扩展委托作为会话的委托(为什么不能使用另一个对象作为委托,我不能说,但这似乎是一个关键细节)。请注意,使用相同的标识符创建第二个 URL 会话允许系统将会话连接到它在另一个进程中为您执行的下载;第二个后台 URL 会话的目的仅仅是将代理与会话连接起来。
  5. 在会话委托中,实现urlSession(_ downloadTask: didFinishDownloadingTo:)urlSession(task: didCompleteWithError:)函数。

    与基于块的 不同NSURLSessionDataTask,后台 URL 请求始终作为下载任务执行。系统执行请求并为您提供一个包含结果数据的临时文件。在urlSession(_ downloadTask: didFinishDownloadingTo:)函数中,该文件中的数据并根据需要对其进行处理以更新您的 UI。

    最后,在委托的urlSession(task: didCompleteWithError:)函数中,调用setTaskCompletedWithSnapshot来告诉系统你已经完成了你的工作。呸。

正如我所提到的,调试起来真的很令人沮丧,主要是因为当这些事情真正发生时,如果它们真的发生的话,这完全取决于系统。Apple 的文档对分配给后台刷新的预算有这样的说法:

一般来说,系统大约每小时为 Dock 中的每个应用程序(包括最近使用的应用程序)执行一项任务。此预算在扩展坞上的所有应用程序之间共享。系统每小时为每个应用程序执行多项任务,并在活动表盘上显示复杂功能。此预算在表盘上的所有复杂功能中共享。在您用尽预算后,系统会延迟您的请求,直到有更多时间可用。

最后一点:传说 watchOS 模拟器不能正确处理后台 URL 刷新任务,但不幸的是,Apple 的文档对此没有官方消息。如果可以的话,最好在 Apple Watch 硬件上进行测试。


推荐阅读