Pull to refresh

Пишем для Apple Watch что-нибудь сложнее Hello, world

Reading time 6 min
Views 12K
Уже прошло много времени, с моменты выпуска компании 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.
Tags:
Hubs:
+15
Comments 5
Comments Comments 5

Articles