Планирование обновления сложности - PullRequest
5 голосов
/ 07 февраля 2020

У меня есть нестандартное осложнение в Apple Watch, которое я пытаюсь обновить один раз в час. Каждый час он должен пропинговать конечную точку API, и если данные изменились с момента последней проверки, сложность должна быть обновлена.

Вот то, что у меня сейчас есть, и, похоже, работает только один раз в голубой луне. Когда он работает, он действительно пингует мой сервер и обновляет усложнение. Похоже, 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;
    }
}

1 Ответ

1 голос
/ 30 марта 2020

фоновые задачи watchOS чрезвычайно громоздки для реализации и отладки, но на основе документации и реализаций Apple, которые обсуждали другие, вот что я считаю лучшей практикой. Здесь я вижу пару проблем.

Во-первых, из документов WKRefreshBackgroundTask :

Система приостанавливает расширение, как только все фоновые задачи завершены.

Вызов setTaskCompletedWithSnapshot для задачи указывает системе, что вы закончили выполнять всю работу, которую вам нужно сделать, поэтому она приостановит ваше приложение. Ваш updateComplicationServer метод, вероятно, никогда не получит шанса на запуск, потому что система слишком рано приостанавливает ваше расширение.

Что еще более важно, чтобы отправлять URL-запросы во время фонового обновления, вам потребуется использовать фоновый сеанс URL-адреса. , Пример процесса , описанный в документах WKRefreshBackgroundTask , показывает наилучшую практику для его настройки. Вкратце:

  1. Вы назначаете фоновый refre sh, используя WKExtension scheduleBackgroundRefresh, как вы делаете.
  2. Система разбудит ваше расширение через некоторое время после предпочитаемую вами дату (по усмотрению системы) с помощью WKRefreshBackgroundTask.
  3. . В методе вашего дополнительного делегата handle проверьте наличие WKApplicationRefreshBackgroundTask; вместо выполнения запроса с URLSessionDataTask здесь вам нужно запланировать сеанс URL background , чтобы система могла приостановить ваш добавочный номер и выполнить запрос от вашего имени. См. WKURLSessionRefreshBackgroundTask документы для получения подробной информации о том, как следует устанавливать фоновые сеансы.
  4. Система выполнит ваш запрос URL в отдельном процессе и снова разбудит ваше расширение, как только оно получит законченный. Он будет вызывать метод вашего делегата расширения handle, как и раньше, на этот раз с WKURLSessionRefreshBackgroundTask. Здесь вам нужно сделать две вещи:

    • Сохранить фоновую задачу в переменной экземпляра в вашем делегате расширения. Мы пока не хотим устанавливать его завершение, но нам нужно сохранить его, чтобы завершить его позже, когда запрос URL завершится.
    • Создайте еще один сеанс фонового URL, используя sessionIdentifier фоновой задачи и используйте ваш делегат расширения в качестве делегата сеанса (почему не получается использовать другой объект в качестве делегата, я не могу сказать, но это, кажется, важная деталь). Обратите внимание, что использование того же идентификатора для создания второго сеанса URL-адреса позволяет системе подключить сеанс к загрузке, выполненной для вас в другом процессе; цель этого второго фонового сеанса URL-адресов состоит исключительно в том, чтобы соединить делегата с сеансом.
  5. В делегате сеанса реализуйте функции urlSession(_ downloadTask: didFinishDownloadingTo:) и urlSession(task: didCompleteWithError:).

    В отличие от вашего NSURLSessionDataTask на основе блоков, фоновые URL-запросы всегда выполняются как задачи загрузки. Система выполняет запрос и выдает временный файл с результирующими данными. В функции urlSession(_ downloadTask: didFinishDownloadingTo:) данные в этом файле и обрабатываются по мере необходимости для обновления вашего пользовательского интерфейса.

    Наконец, в функции делегата urlSession(task: didCompleteWithError:) вызовите setTaskCompletedWithSnapshot, чтобы сообщить системе, что вы закончил свою работу Фу.

Как я уже упоминал, все это очень сложно отладить, в основном потому, что все зависит от системы, когда эти вещи действительно происходят, если они вообще случаются. В документации Apple говорится о бюджете, выделенном на фоновые обновления:

В целом, система выполняет примерно одну задачу в час для каждого приложения в док-станции (включая последнее использованное приложение). Этот бюджет распределяется между всеми приложениями на док-станции. Система выполняет несколько задач в час для каждого приложения с усложнением на активном циферблате. Этот бюджет распределяется между всеми сложностями на циферблате. После исчерпания бюджета система задерживает ваши запросы до тех пор, пока не станет доступно больше времени.

Последнее замечание: легенда гласит, что имитатор watchOS не обрабатывает фоновые URL-ссылки повторно sh задачами должным образом, но, к сожалению, в документах Apple нет официального слова об этом. Лучше всего протестировать на оборудовании Apple Watch, если вы можете.

...