安排并发症更新

use*_*922 8 apple-watch watchkit apple-watch-complication

我在 Apple Watch 上有一个自定义的复杂功能,我想每小时更新一次。它应该每小时 ping 一个 API 端点,如果上次检查后数据发生了变化,则应该更新复杂性。

这是我目前拥有的似乎只能在蓝色月亮中工作一次的东西。当它工作时,它确实会 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Joh*_*ham 5

watchOS 后台任务的实现和调试非常麻烦,但是基于 Apple\xe2\x80\x99s 文档和其他人讨论过的实现,here\xe2\x80\x98s 我认为是最佳实践。我在这里看到几个问题。

\n\n

首先,来自WKRefreshBackgroundTask 文档

\n\n
\n

所有后台任务完成后,系统会立即暂停扩展。

\n
\n\n

调用setTaskCompletedWithSnapshot该任务向系统表明您\xe2\x80\x99已经完成了需要执行的所有工作,因此它将挂起您的应用程序。你的updateComplicationServer方法可能永远没有机会运行,因为系统过早挂起您的扩展。

\n\n

更重要的是,要在后台更新期间发出 URL 请求,您\xe2\x80\x99 将需要使用后台 URL 会话。WKRefreshBackgroundTask 文档中概述的示例流程了设置此功能的最佳实践。简而言之:

\n\n
    \n
  1. WKExtension您可以使用\xe2\x80\x99s安排后台刷新scheduleBackgroundRefresh就像您\xe2\x80\x99 所做的那样。
  2. \n
  3. 系统将在您首选日期之后的某个时间唤醒您的分机(由系统\xe2\x80\x99s 自行决定),并使用以下命令:WKRefreshBackgroundTask.
  4. \n
  5. 在您的扩展 delegate\xe2\x80\x99shandle方法中,检查WKApplicationRefreshBackgroundTask; URLSessionDataTask您需要安排一个后台URL会话,以便系统可以挂起您的扩展并代表您执行请求,而不是在此处执行请求。请参阅WKURLSessionRefreshBackgroundTask有关如何设置后台会话的详细信息,
  6. \n
  7. 系统将在单独的进程中执行您的 URL 请求,并在完成后再次唤醒您的扩展程序。它会像以前一样调用您的扩展 delegate\xe2\x80\x99shandle方法,这次使用WKURLSessionRefreshBackgroundTask. 在这里,您需要做两件事:

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

    \n\n

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

    \n\n

    最后,在 delegate\xe2\x80\x99surlSession(task: didCompleteWithError:)函数中,调用setTaskCompletedWithSnapshot告诉系统你\xe2\x80\x99已经完成了你的工作。唷。

  10. \n
\n\n

正如我所提到的,这对于调试来说确实令人沮丧,主要是因为当这些事情实际发生时(如果它们真的发生),一切都取决于系统。Apple\xe2\x80\x99s 文档对于分配给后台刷新的预算有这样的说法:

\n\n
\n

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

\n
\n\n

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

\n