Пишем для 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.

ссылка на оригинал статьи http://habrahabr.ru/post/262075/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *