我在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;
}
}
答案 0 :(得分:2)
watchOS后台任务非常难以实施和调试,但是基于Apple的文档和其他人已经讨论过的实现,我认为这是最佳实践。我在这里看到了几个问题。
首先,从WKRefreshBackgroundTask docs:
所有后台任务完成后,系统会立即暂停扩展程序。
在任务上调用setTaskCompletedWithSnapshot
表示系统已完成所需的所有工作,因此将挂起您的应用程序。您的updateComplicationServer
方法可能永远都没有机会运行,因为系统过早挂起了扩展程序。
更重要的是,要在后台更新期间发出URL请求,您需要使用后台URL会话。 example process outlined in the WKRefreshBackgroundTask docs显示了进行此设置的最佳实践。简而言之:
WKExtension
的{{1}}安排后台刷新。scheduleBackgroundRefresh
唤醒您的扩展程序。WKRefreshBackgroundTask
方法中,检查handle
;而不是在此处使用WKApplicationRefreshBackgroundTask
来执行请求,您需要安排一个 background URL会话,以便系统可以暂停您的扩展并代表您执行请求。有关应如何设置后台会话的详细信息,请参见WKURLSessionRefreshBackgroundTask文档。系统将在一个单独的过程中执行您的URL请求,并在扩展完成后再次唤醒您。它将像以前一样调用您的扩展程序代表的URLSessionDataTask
方法,这次使用handle
。在这里,您需要做两件事:
WKURLSessionRefreshBackgroundTask
创建另一个后台URL会话,并使用扩展程序委托作为会话的委托(为什么我不能将另一个对象用作委托,我不能说,但这似乎是至关重要的细节)。请注意,使用相同的标识符创建第二个URL会话,系统可以将会话连接到它在另一个过程中为您执行的下载。第二个后台URL会话的目的仅仅是将委托与会话连接。在会话委托中,同时实现sessionIdentifier
和urlSession(_ downloadTask: didFinishDownloadingTo:)
函数。
与基于块的urlSession(task: didCompleteWithError:)
不同,后台URL请求始终作为下载任务执行。系统执行请求,并为您提供包含结果数据的临时文件。在NSURLSessionDataTask
函数中,该文件中的数据并根据需要进行处理以更新您的UI。
最后,在委托人的urlSession(_ downloadTask: didFinishDownloadingTo:)
函数中,调用urlSession(task: didCompleteWithError:)
来告诉系统您已经完成工作。 ew。
正如我提到的那样,调试真的很令人沮丧,主要是因为这些事情实际发生时(如果它们确实发生)完全取决于系统。 Apple的文档中有关于分配给后台刷新的预算的说法:
通常,系统每小时为扩展坞中的每个应用程序(包括最近使用的应用程序)执行大约一项任务。该预算由扩展坞上的所有应用共享。该系统每小时为每个应用程序执行多项任务,并在活动表盘上进行复杂处理。此预算由表盘上的所有复杂功能分配。用完预算后,系统会延迟您的请求,直到有更多时间可用为止。
最后要注意的一点是:传说watchOS模拟器不能正确处理后台URL刷新任务,但是不幸的是,Apple的文档对此没有官方的说法。如果可以的话,最好在Apple Watch硬件上进行测试。