Пишем для Apple Watch что нибудь сложнее Hello, world
Уже прошло много времени, с моменты выпуска компании Apple ее нового продукта — часов Apple Watch. Уже скоро выйдет финальная версия операционной системы для них — Watch OS 2.0. А на Хабре до сих пор нет более-менее развернутой статьи о том, как написать что нибудь сложнее «Hello, world!» для Apple Watch. И в этой статье мы постараемся это исправить и написать приложение из нескольких экранов со списком, загрузкой данных и взаимодействием с основным приложением.
В тех статьях, что уже писали про Apple Watch, уже подробно описывался принцип из работы, рисовались схемы, разбирались достоинства и недостатки. Поэтому я предлагаю не останавливаться на этом подробно, а сразу приступить к работе!
Единственное, что стоит упомянуть, так это то, что приложение выполняется на телефоне, а часы просто отображают пользовательский интерфейс. В целом, все это объясняется картинкой из официальной документации. Думаю, те кто интересуется разработкой для Apple Watch, видели ее уже очень много раз :)
А теперь уже можно приступить, и первым делом в нашем приложении мы должны создать расширение для работы с часами WatchKit Extension. Делает это очень просто: в списке Target«ов кликаете на плюсик, находите там WatchKit App. На следующем экране проверяем что там стоит наше основное приложение, прописаны правильные BundleID и нажимаем Finish.
После этих манипуляций в проекте появится два новых Target«а: расширение для нашего основного приложения и само приложение для часов.
Так же, в дереве проекта добавилось две папки (для расширения и для приложения соответственно) и в папке для приложения есть знакомый нам Interface.storyboard.
Тут есть привычные нам контроллеры, которые теперь являются наследниками класса WKInterfaceController, есть компоненты UI (которых пока к сожалению очень мало) и есть переходы между экранами Segue.
Основной момент, на который стоит обратить внимание — если на iOS был один вход (Initital controller), то теперь их сразу три — начальный экран, уведомления и «glance» (еще один из вариантов уведомлений).
На нашем основной контроллере добавим кнопку (перетащим ее справа-снизу из Object Library. Тут проявляется очередная особенность часов — разработчики максимально упростили возможности интерфейса и компоненты можно располагать только друг за другом, сверху вниз. Если же вам надо расположить компоненты в строчку, то для вас предусмотрели компонент Group, который представляет из себя контейнер других компонентов и у которого есть параметр Layout (либо вертикальный, либо горизонтальный).
Кроме того, компонент внутри родительского контейнера можно выравнивать по вертикали и горизонтали, а так же задавать ему абсолютный и относительный размер.
В целом несложная концепция, которая если подумать, позволяет покрывать большую часть потребностей.
Добавим на основной экран пару кнопок и переход с одной из них на следующий экран, где мы попробуем сделать список. Переходы между экранами происходят так же как в iOS посредством segue. Но есть одна интересная особенность: на вкладке Connections Inspector для контроллера есть свойство Next page. Соединив его с другим контроллером можно делать переходы между ними смахиванием как на Page View Controller.
На следующем экране, мы создадим список данных, загружаемых по сети. В часах нет аналога UITableViewController, т.к. все наследуется от единственного WKInterfaceController, поэтому мы просто переносим компонент Table на наш экран и связываем его с аутлет-свойством контроллера. В компоненте по умолчанию создается прототип строки TableRow.
Рассмотрим теперь подробнее внутренности контроллера. По умолчанию там создается три метода. Рассмотрим их поподробнее.
Метод инициализации экрана, сюда с помощь параметра context передаются данные из других экранов, здесь же необходимо инициализировать данные и загружать их.
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
}
Метод который вызывается после отображения экрана. Тут можно запускать анимации, делать несложные обновления данных.
- (void)willActivate {
[super willActivate];
}
Метод выгружения экрана, где нужно все за собой почистить, сохранить и убрать.
- (void)didDeactivate {
[super didDeactivate];
}
Теперь в методе awakeWithContext мы попробуем заполнить наш список данными. Для этого нужно написать класс ячейки списка, причем наследовать его нужно от обычного NSObject. У меня он выглядит так:
@interface WKRow : NSObject
@property (weak, nonatomic) IBOutlet WKInterfaceLabel *rowTitle;
@end
А в InterfaceBuilder«е мы указываем его как класс ячейки и связываем свойства с UI компонентами. Так же, у ячейки в InterfaceBuilder«е нужно указать Identifier по которому она будет создаваться.
После этого, код добавления данных в таблицу будет выглядеть следующим образом:
[self.table setNumberOfRows:items.count withRowType:@"itemRow"];
for (Item *itm in items){
WKRow *row = [self.table rowControllerAtIndex:i];
[row.rowTitle setText:itm.title];
}
setNumberOfRows создает в таблице необходимое нам количество ячеек с нужным идентификатором, который мы указали в IB. А дальше мы можем обратиться к каждой ячейке с помощью метода rowControllerAtIndex, который вернет нам наш класс контроллера ячейки.
Раньше был такой баг, что при частом вызове setNumberOfRows интерфейс часов начинал себя вести очень странно (тормозить и глючить). Но в последней версии это поправили, по крайней мере я у себя это повторить не смог.
Теперь, появляется вопрос — можно ли нам взять данные которые уже загружены в наше основное приложение, и которые не хочется грузить повторно в Extension?
И тут можно рассказать про еще одно базовое понятие — взаимодействие WatchKit приложения с родительским приложением iOS. Смысл в следующем: мы отправляем «запрос» в родительское приложение, оно достает данные из базы данных, или загружает из из сети, и возвращает нам их обратно.
Работает это все с помощью метода контроллера openParentApplication. В качестве параметра мы передаем словарь с параметром action и в reply указываем блок в котором обрабатываем ответ.
[WBridgeInterfaceController openParentApplication:@{
@"action":@"remind"
}
reply:^(NSDictionary *replyInfo, NSError *error){
NSLog(@"Result: %@", [replyInfo objectForKey:@"result"]);
}];
В родительском приложении, в делегате приложения мы переопределяем метод handleWatchKitExtensionRequest. В нем мы получаем словарик, отправленный из часов, обработав который, можем вернуть результат в блок обработки так же, с помощью словаря.
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply{
[BridgeReminder setRemind];
reply(@{
@"result": @"ok"
});
}
После загрузки и отображения данных, попробуем обработать тап по строчке списка и отобразить следующий экран, например с подробной информацией об объекте.
Самый простой способ открывать экран при тапе на строчку — открыть Storyboard и в Connection Inspector«е связать пункт selection у компонента строки с контроллером на который нужно перейти.
Далее в Attribute Inspector«е указываем у него Identifier, чтобы мы могли обратиться к переходу при срабатывании и передать данные.
В самом контроллере, мы переопределяем метод contextForSegueWithIdentifier, который срабатываем при старте segue и в нем, проверив по идентификатору что это нужный нам переход, возвращаем указатель на данные которые нужно передать следующему экрану. Например так:
- (id)contextForSegueWithIdentifier:(NSString *)segueIdentifier inTable:(WKInterfaceTable *)table rowIndex:(NSInteger)rowIndex{
if ([segueIdentifier isEqualToString:@"openCard"]){
return [items objectAtIndex:rowIndex];
}
return nil;
}
В контроллере следующего экрана, в уже знакомом нам методе awakeWithContext, мы эти данные можем получить и использовать как нам вздумается, например отобразить :)
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
Item *b = context;
[self.itemTitle setText: b.title];
}
В процессе написания статьи заметил такую странную «фичу» Xcode — если писать идентификатор Segue с заглавными буквами, например «showInfo», то его нельзя поймать методом contextForSegueWithIdentifier, т.к. такой идентификатор не сохранится и у перехода будет имя сгенерированное самой IDE.
Еще из особенностей разработки, впервые пришлось столкнуться с тем, что некоторые pod«ы не предназначены для использования в расширениях и при сборке начинают ругаться. Например, AFNetworking писал что не знает что такое [UIApplication sharedApplication], т.к. в расширениях этот класс недоступен.
Решается все очень просто — добавляем параметр AF_APP_EXTENSIONS=1 в макросы сборки.
У каждого pod«а название параметра может быть свое и обычно либо прописан в документации, либо находиться перед строчкой на которую ругается компилятор при сборке.
На этом, предлагаю остановиться и попробовать вам написать свое первое приложение для Apple Watch. А я в следующей части расскажу подробнее как работают нотификации и экран Glance.