安排并发症更新 [英] Scheduling complication updates

查看:31
本文介绍了安排并发症更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a custom complication on Apple Watch that I am trying to get to update once an hour. Each hour it should ping an API endpoint and if the data has changed from the last check, the complication should be updated.

这是我目前所拥有的,似乎只能在蓝月亮中工作一次.当它工作时,它确实会 ping 我的服务器并更新并发症.看来 WatchOS 并没有每小时调用一次我的预定任务.是否有我遗漏的更好的标准做法?

Here is what I currently have that only seems to work once in a blue moon. When it DOES work, it does indeed ping my server and update the complication. It appears WatchOS just isn't calling my scheduled tasked once per hour. Is there a better standard practice that I'm missing?

@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;
    }
}

推荐答案

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

watchOS background tasks are extraordinarily cumbersome to implement and debug, but based on Apple’s documentation and implementations that others have discussed, here‘s what I believe to be the best practice. I see a couple of issues here.

首先,来自 WKRefreshBackgroundTask 文档:

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

The system suspends the extension as soon as all background tasks are complete.

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

Calling setTaskCompletedWithSnapshot on the task indicates to the system that you’ve finished performing all the work you need to do, so it will suspend your app. Your updateComplicationServer method is probably never getting a chance to run because the system suspends your extension too early.

更重要的是,要在后台更新期间发出 URL 请求,您需要使用后台 URL 会话.WKRefreshBackgroundTask 文档中概述的示例流程 展示了设置此功能的最佳做法.简而言之:

More importantly, to make URL requests during a background update, you’ll need to use a background URL session. The example process outlined in the WKRefreshBackgroundTask docs shows the best practice for setting this up. In short:

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

  1. You schedule a background refresh using WKExtension’s scheduleBackgroundRefresh just like you’re doing.
  2. The system will wake your extension sometime after your preferred date (at the system’s discretion) with a WKRefreshBackgroundTask.
  3. In your extension delegate’s handle method, check for the WKApplicationRefreshBackgroundTask; instead of performing the request with a URLSessionDataTask here, you need to schedule a background URL session so that the system can suspend your extension and perform the request on your behalf. See the WKURLSessionRefreshBackgroundTask docs for details about how background sessions should be set up.
  4. The system will perform your URL request in a separate process, and again wake your extension once it has finished. It will call your extension delegate’s handle method like before, this time with a WKURLSessionRefreshBackgroundTask. Here, you need to do two things:

  • 将后台任务保存在扩展委托的实例变量中.我们暂时不想将其设置为完成,但我们需要保留它以便稍后在 URL 请求完成时设置完成.
  • 使用后台任务的 sessionIdentifier 创建另一个后台 URL 会话,并使用您的扩展委托作为会话的委托(为什么不能使用另一个对象作为委托,我不能说,但这似乎是一个关键的细节).请注意,使用相同的标识符创建第二个 URL 会话允许系统将该会话连接到它在另一个进程中为您执行的下载;这第二个后台 URL 会话的目的仅仅是将代理与会话连接起来.
  • Save the background task in an instance variable on your extension delegate. We don’t want to set it complete just yet, but we need to keep it around to set complete later when the URL request finishes.
  • Create another background URL session using the background task’s sessionIdentifier, and use your extension delegate as the session’s delegate (why it doesn’t work to use another object as the delegate, I can’t say, but it seems to be a crucial detail). Note that using the same identifier to create a second URL session allows the system to connect the session to the download that it performed for you in another process; the purpose of this second background URL session is solely to connect the delegate with the session.

在会话委托中,同时实现 urlSession(_ downloadTask: didFinishDownloadingTo:)urlSession(task: didCompleteWithError:) 函数.

In the session delegate, implement both the urlSession(_ downloadTask: didFinishDownloadingTo:) and urlSession(task: didCompleteWithError:) functions.

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

Unlike your block-based NSURLSessionDataTask, background URL requests are always performed as download tasks. The system performs the request and gives you a temporary file with the resulting data. In the urlSession(_ downloadTask: didFinishDownloadingTo:) function, the data in that file and process it as needed to update your UI.

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

Finally, in the delegate’s urlSession(task: didCompleteWithError:) function, call setTaskCompletedWithSnapshot to tell the system that you’ve finished your work. Phew.

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

As I mentioned, this is all really frustrating to debug, mostly because it’s all up to the system when these things actually take place, if they happen at all. Apple’s documentation has this to say about the budget allocated to background refreshes:

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

In general, the system performs approximately one task per hour for each app in the dock (including the most recently used app). This budget is shared among all apps on the dock. The system performs multiple tasks an hour for each app with a complication on the active watch face. This budget is shared among all complications on the watch face. After you exhaust the budget, the system delays your requests until more time becomes available.

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

One final note: legend has it that the watchOS simulator doesn’t handle background URL refresh tasks properly, but unfortunately, Apple’s docs have no official word on that. Best to test on Apple Watch hardware if you‘re able.

这篇关于安排并发症更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆