Статьи

Двунаправленная связь между расширением Apple Watch и приложением хоста

Это чрезвычайно важно, так как Apple Watch обеспечивает второй экран / дополнительное периферийное оборудование для основного приложения, работающего на устройстве iOS — будь то пульт дистанционного управления или быстрый взгляд / взгляд на то, что происходит внутри более широкой картины.

В своем последнем посте я показал, как настроить удаленную регистрацию и инструментарий / аналитику в приложении Apple Watch с использованием сервера IBM MobileFirst Platform Foundation . Я использовал методы, описанные ниже, для связи между WatchKit и хост-приложениями в примере приложения из этого предыдущего поста.

Когда мы говорим о двунаправленной связи, мы говорим о передаче данных двумя способами:

  1. Отправка данных из хост-приложения в приложение WatchKit
  2. Отправка данных в приложение WatchKit из хост-приложения

Сначала вы можете подумать: «О, это просто, просто используйте  NSNotificationCenter для связи между отдельными классами приложения», но все не так просто.

Приложение Apple Watch действительно состоит из 3 частей: 1) двоичного файла основного приложения iOS, 2) пользовательского интерфейса на Apple Watch и 3) двоичного файла расширения WatchKit (на устройстве iOS).

Apple Watch App — Архитектурные компоненты

Да, вы правильно прочитали, расширение WatchKit (которое контролирует всю логику в пользовательском интерфейсе Apple Watch и находится на устройстве iOS) представляет собой отдельный двоичный файл из «основного» двоичного файла приложения iOS. Это отдельные процессы, поэтому объекты в памяти в основном приложении не являются одними и теми же объектами в памяти в расширении, и в результате эти процессы не взаимодействуют напрямую. NSNotificationCenter не будет работать.

Однако, есть определенные способы заставить этот тип сценария работать.

Во-первых, в WatchKit есть методы для вызова действий в хост-приложении из расширения WatchKit. Методы  openParentApplication  или handleWatchKitExtensionRequest WatchKit предоставляют возможность вызывать действия и передавать данные в содержащем приложении, а также предоставляют механизм для вызова блока кода «ответ» обратно в расширении WatchKit после завершения выполнения кода в главном приложении.

Например, в расширении WatchKit это вызовет действие в хост-приложении и обработает ответ:

[WKInterfaceController openParentApplication:@{@"action":@"toggleStatus"} reply:^(NSDictionary *replyInfo, NSError *error) {
    [logger trace:@"toggleStatus reply"];
    [self updateUIFromHost:replyInfo];
}];

Внутри хост-приложения у нас есть доступ к userInfo NSDictionary, который был передан, и мы можем ответить на него соответствующим образом. Например, в приведенном ниже коде я устанавливаю строковое значение для экземпляра userInfo и предпринимаю соответствующие действия на основе значения этой строки.

- (void)application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
  reply:(void (^)(NSDictionary *replyInfo))reply {

  //handle this as a background task
  __block UIBackgroundTaskIdentifier watchKitHandler;
  watchKitHandler = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"backgroundTask"
            expirationHandler:^{
              watchKitHandler = UIBackgroundTaskInvalid;
            }];

  NSString *action = (NSString*) [userInfo valueForKey:@"action"];
  [logger trace:@"incoming request from WatchKit: ", action];

  LocationManager * locationManager = [LocationManager sharedInstance];

  NSMutableDictionary *result = [[NSMutableDictionary alloc] init];

  if ([action isEqualToString:@"toggleStatus"]) {
    //toggle whether or not we're actually tracking the location
    [locationManager toggleTracking];
  } else if ([action isEqualToString:@"stopTracking"]) {
    [locationManager stopTracking];
  } else if ([action isEqualToString:@"currentStatus"]) {
    //do nothing for now
  }

  NSString *trackingString = [NSString stringWithFormat:@"%s", locationManager.trackingActive ? "true" : "false"];
  [result setValue:trackingString forKey:@"tracking"];
  reply(result);

  dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)NSEC_PER_SEC * 1 ), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
    [[UIApplication sharedApplication] endBackgroundTask:watchKitHandler];
  } );
}

Это относится к сценарию «вытягивания» и отлично подходит, если вы хотите вызывать действия в вашем хост-приложении из расширения WatchKit, а затем обрабатывать ответы обратно в расширении WatchKit, чтобы соответствующим образом обновить пользовательский интерфейс Apple Watch.

А как насчет сценария «толчка»? Предыдущий сценарий охватывает только запросы, которые исходят из расширения WatchKit. Что произойдет, если у вас внутри вашего хост-приложения запущен процесс, и у вас есть обновления, которые вы хотите отправить в расширение WatchKit без исходящего запроса?

Нет общей памяти, и это не общий процесс, поэтому не будет работать ни NSNotificationCenter, ни прямой вызов метода. Однако вы * можете * использовать уведомления Дарвина (которые работают в отдельных процессах с помощью  CFNotificationCenter ). Это обеспечивает взаимодействие между процессами практически в реальном времени, и вы можете обмениваться данными в качестве атрибутов объекта CFdictionary, основанного на процессах. Вы также можете обмениваться большими объемами данных с помощью групп доступа и уведомлять отдельные процессы с помощью реализации CFNotificationCenter.

Примечание: CFNotificationCenter — это синтаксис языка C, а не синтаксис Objective-C.

Сначала вам нужно подписаться на уведомления в WatchKitExtension . Обратите внимание на экземпляр статического идентификатора «staticSelf»… это понадобится вам позже при вызове методов Objective-C из обратного вызова уведомлений C.

static id staticSelf;

- (void)awakeWithContext:(id)context {
  [super awakeWithContext:context];

  //add your initialization stuff here

  CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), didReceiveTrackingStatusNotificaiton, CFSTR("TrackingStatusUpdate"), NULL, CFNotificationSuspensionBehaviorDrop);

  staticSelf = self;
}

Из вашего хост-приложения вы можете вызвать CFNotificationCenterPostNotification для вызова дарвиновского уведомления.

-(void) postTrackingStatusNotificationToWatchKit {

  NSString *trackingString = [NSString stringWithFormat:@"%s", self.trackingActive ? "true" : "false"];

  NSDictionary *payload = @{@"tracking":trackingString};
  CFDictionaryRef cfPayload = (__bridge CFDictionaryRef)payload;

  CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFSTR("TrackingStatusUpdate"), (__bridge const void *)(self), cfPayload, TRUE);
}

Затем в расширении WatchKit обработайте уведомление и соответствующим образом обновите свое расширение WatchKit.

void didReceiveTrackingStatusNotificaiton() {
  [staticSelf respondToPostFromHostApp];
}

Теперь мы рассмотрели сценарии, в которых вы можете запрашивать данные или действия в хост-приложении * из * расширения WatchKit, а также способы передачи данных из хост-приложения в расширение WatchKit.

Теперь, что, если бы была библиотека, которая инкапсулировала некоторые из них, и сделала ее еще проще для разработчика? Когда я писал приложение в своем предыдущем посте, я использовал методы, описанные выше. Однако недавно я наткнулся на MMWormhole с открытым исходным  кодом , которая оборачивает метод Darwin Notifications (см. Выше) для простоты использования. Я уверен, что буду использовать это в своем следующем приложении WatchKit.