watchkit - 安排并发症更新
问题描述
我在 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;
}
}
解决方案
watchOS 后台任务的实现和调试异常繁琐,但根据其他人讨论过的 Apple 文档和实现,我认为这是最佳实践。我在这里看到了几个问题。
首先,来自WKRefreshBackgroundTask 文档:
一旦所有后台任务完成,系统就会暂停扩展。
调用setTaskCompletedWithSnapshot
任务向系统表明您已经完成了您需要做的所有工作,因此它将暂停您的应用程序。您的updateComplicationServer
方法可能永远没有机会运行,因为系统过早地暂停了您的扩展。
更重要的是,要在后台更新期间发出 URL 请求,您需要使用后台 URL 会话。WKRefreshBackgroundTask 文档中概述的示例过程显示了设置它的最佳实践。简而言之:
WKExtension
您可以使用'来安排后台刷新scheduleBackgroundRefresh
,就像您正在做的那样。- 系统将在您首选日期之后的某个时间(由系统自行决定)唤醒您的分机,并带有
WKRefreshBackgroundTask
. - 在您的扩展委托的
handle
方法中,检查WKApplicationRefreshBackgroundTask
; 而不是在这里执行请求URLSessionDataTask
,您需要安排一个后台URL 会话,以便系统可以暂停您的扩展程序并代表您执行请求。有关如何设置后台会话的详细信息,请参阅WKURLSessionRefreshBackgroundTask文档。 系统将在单独的进程中执行您的 URL 请求,并在完成后再次唤醒您的扩展程序。它将
handle
像以前一样调用您的扩展委托的方法,这次使用WKURLSessionRefreshBackgroundTask
. 在这里,您需要做两件事:- 将后台任务保存在扩展委托的实例变量中。我们还不想将其设置为完成,但我们需要保留它以便稍后在 URL 请求完成时设置完成。
- 使用后台任务创建另一个后台 URL 会话
sessionIdentifier
,并使用您的扩展委托作为会话的委托(为什么不能使用另一个对象作为委托,我不能说,但这似乎是一个关键细节)。请注意,使用相同的标识符创建第二个 URL 会话允许系统将会话连接到它在另一个进程中为您执行的下载;第二个后台 URL 会话的目的仅仅是将代理与会话连接起来。
在会话委托中,实现
urlSession(_ downloadTask: didFinishDownloadingTo:)
和urlSession(task: didCompleteWithError:)
函数。与基于块的 不同
NSURLSessionDataTask
,后台 URL 请求始终作为下载任务执行。系统执行请求并为您提供一个包含结果数据的临时文件。在urlSession(_ downloadTask: didFinishDownloadingTo:)
函数中,该文件中的数据并根据需要对其进行处理以更新您的 UI。最后,在委托的
urlSession(task: didCompleteWithError:)
函数中,调用setTaskCompletedWithSnapshot
来告诉系统你已经完成了你的工作。呸。
正如我所提到的,调试起来真的很令人沮丧,主要是因为当这些事情真正发生时,如果它们真的发生的话,这完全取决于系统。Apple 的文档对分配给后台刷新的预算有这样的说法:
一般来说,系统大约每小时为 Dock 中的每个应用程序(包括最近使用的应用程序)执行一项任务。此预算在扩展坞上的所有应用程序之间共享。系统每小时为每个应用程序执行多项任务,并在活动表盘上显示复杂功能。此预算在表盘上的所有复杂功能中共享。在您用尽预算后,系统会延迟您的请求,直到有更多时间可用。
最后一点:传说 watchOS 模拟器不能正确处理后台 URL 刷新任务,但不幸的是,Apple 的文档对此没有官方消息。如果可以的话,最好在 Apple Watch 硬件上进行测试。
推荐阅读
- r - 在 echarts4r 中删除千位逗号
- anylogic - 限制由 if else 函数选择的托盘架的空间
- android - RecyclerView 中的 BaseAdapter 仅显示一项问题
- spring - 如何测试 commonsMultipartFile 文件?
- python - 无法通过 SubProcess 库对 Python Flask 服务进行后台处理
- r - 对于 R 中的大矩阵,如何有效地计算所有可能组合中的归一化比率?
- python - 如何使用python获取特定的文本表单xml文件?
- reactjs - 单击提交按钮期间未触发验证
- node.js - Gremlin Javascript 按功能排序
- ios - 如何找出哪个约束不起作用或产生问题