[Из песочницы] Создаем первое приложение для Apple watchOS 2

Совсем недавно, в сентябре, Apple выпустила ожидаемый многими апдейт, вторую версию операционной системы watchOS. По написанию программ для нее статей на Хабре вроде еще не было, попробуем исправить этот момент.

Что нового


Все знают, что ключевым недостатком всех «умных часов» является малое время работы от батарей. Инженеры Apple решили исправить этот момент весьма простым способом — максимально разгрузить процессор часов. Для этого было придумано оригинальное решение — на часах хранились лишь ресурсы программы, а все вычисления делались на процессоре телефона. Программа для часов состояла из 2х компонентов: Watch App (то что хранится на часах) и WatchKit Extention (хранится на телефоне). Т.е. по сути, это было неким вариантом «удаленного рабочего стола» для смартфона — без наличия рядом телефона приложение работать не могло. В качестве канала связи скорее всего, использовался не отличающийся быстротой Bluetooth LE. Пользователи в итоге часто жаловались на «заторможенность» интерфейса, что было следствием этого принципа. В общем, несмотря на оригинальность, концепция «не взлетела». Так вот, основное отличие OS2 — теперь приложение хранится на часах полностью. Однако абсолютно независимым оно все равно не является — хотя приложение на часах работает автономно, должна быть «основная» программа для iPhone, вместе с которой это приложение ставится. Все это сильно похоже на «костыль», и в следующей версии такого ограничения наверно не будет. Впрочем, посмотрим.

Если говорить о железе, то программисту доступны 2 варианта часов для отладки:

— экран 38mm, разрешение 272x340,
— экран 42мм, разрешение 312x390.

Остальных характеристик (память, процессор и пр), на сайте Apple их найти не удалось. Впрочем, для нашего проекта это не столь важно. Перейдем к проекту (осторожно, траффик).

Создаем проект


Как и все приложения для Mac OS, iOS, программы для watchOS создаются в среде Xcode. Скачать ее бесплатно можно в Mac App Store. Итак, выбираем в Xcode создание нового проекта. В версии 7.0 появился новый template iOS App with WatchKit App.

b45add3c5f2e4d9ea079ad10c6b0afc1.jpg

Создаем приложение, назовем его WatchTest. Xcode создает проект, как показано на рисунке.

5d299871dd0547a8a02f2b151452f2d8.jpg

Как можно видеть, проект состоит из 3х частей:

— WatchTest — основное приложение для iOS,
— WatchKit App — часть приложения, хранящее ресурсы,
— WatchKit Extention — часть приложения, хранящая код.

(Видно, что разделение на 2 части осталось, что похоже на «костыль» N2, но теперь по крайней мере, обе части хранятся на часах, и задержки связанной с передачей сигнала на смартфон и обратно, не будет).

Создаем приложение iOS


Для учебных целей создадим что-то несложное, например программу-секундомер. Один Label выводящий время, и 2 кнопки «старт-стоп» и «сброс». Нажатие кнопки старт запускает таймер, инкрементирующий количество долей секунды. Опустим много промежуточных деталей, покажем сразу результат. Код (на Swift) и ресурсы показаны на рисунке.

6518604750d24c6cac1a5c5b18aed43b.jpg

Здесь можно видеть контролы (с тэгом @IBOutlet, в нашем случае только один UILabel) и обработчики 2х кнопок с тегом @IBAction (кому интересно подробнее, можно почитать например здесь).

Исходный код этой части (после небольшого рефакторинга)
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var timeLabel: UILabel!
    var seconds: Int!
    var timer: NSTimer!
    let interval: Double = 1.0/20.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.seconds = 0;
        
        // Watch support
        initWatchConnection()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func onButtonStart(sender: AnyObject) {
        startOrStopTimer()
    }
    
    @IBAction func onButtonReset(sender: AnyObject) {
        resetTimer()
    }
    
    func startOrStopTimer() {    
        if (self.timer == nil) {
            self.timer = NSTimer.scheduledTimerWithTimeInterval(interval, target:self,
                selector:"onTimer", userInfo:nil,
                repeats:true)
        } else {
            self.timer.invalidate()
            self.timer = nil
        }
    }
    
    func resetTimer() {    
        if (self.timer != nil) {
            self.timer.invalidate()
            self.timer = nil
        }
        self.seconds = 0;
        self.timeLabel.text = "0.00"
    }
    
    func onTimer() {
        
        self.seconds = self.seconds + 1
        self.timeLabel.text = String(format: "%.2f", Double(self.seconds)*interval)
    }
}



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

Создаем приложение Apple Watch


Открываем interface.storyboard на WatchKit App. Добавляем 2 кнопки и Label для вывода результата. В коде создаем объект класса WCSession, который и отвечает за коннект часов с телефоном. У этого класса есть метод sendCommand, которым мы и воспользуемся. Его параметром является dictionary, в который мы можем поместить различные данные. В нашем случае мы будем передавать обычную текстовую команду. Способ может не самый эффективный, зато простой и наглядный.

Итоговый результат (ресурсы + код) показан на рисунке.

9ef0770e371a4266b49fe7ee8e69c2f8.jpg

Исходный код этой части
import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {
    
    @IBOutlet var statusLabel: WKInterfaceLabel!
    private var session : WCSession!

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    }

    override func willActivate() {
        super.willActivate()
        
        session = WCSession.isSupported() ? WCSession.defaultSession() : nil
        session?.delegate = self
        session?.activateSession()
    }

    override func didDeactivate() {
        session = nil
        super.didDeactivate()
    }
    
    @IBAction func onButtonStartStop() {
        self.sendCommand("startstop")
    }
    
    @IBAction func onButtonReset() {
        self.sendCommand("reset")
    }
    
    func sendCommand(cmd: String) {
        self.statusLabel.setText("Sending...")
        if let session = session where session.reachable {
            
            let applicationData = [ "body" : cmd ]
            session.sendMessage(applicationData,
                                replyHandler: { replyData in
                                    self.statusLabel.setText("Send: done")
                                }, errorHandler: { error in
                                    self.statusLabel.setText("Send: fail")
                                })
        } else {
            self.statusLabel.setText("No connection")
        }
    }
}



Запускаем, нажимаем кнопки… и ничего не происходит. Правильно, мы не добавили обработчик сообщений от часов в основную программу.

Дорабатываем приложение iOS


Добавляем в приложение iOS практически идентичный код для инициализации WCSession. Интерес для нас представляет функция session:didReceiveMessage, которая автоматически вызовется, как только приложение получит сообщение от часов. Параметром этой функции будет тот самый dictionary, который мы передали при отправке. В replyHandler мы можем передать любой ответ, который уже на часах может быть проанализирован. Функция dispatch_async(dispatch_get_main_queue()) нужна для того, чтобы работа с таймером осуществлялась в основном потоке, иначе он не запускается корректно.

Окончательный вариант кода виден на рисунке.

a2fd5b381e304934a64989e6bc9a0316.jpg

Исходный код полностью
import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var timeLabel: UILabel!
    var seconds: Int!
    var timer: NSTimer!
    let interval: Double = 1.0/20.0
    private var session : WCSession!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.seconds = 0;
        
        // Watch support
        initWatchConnection()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func onButtonStart(sender: AnyObject) {
        startOrStopTimer()
    }
    
    @IBAction func onButtonReset(sender: AnyObject) {
        resetTimer()
    }
    
    func startOrStopTimer() {
    
        if (self.timer == nil) {
            self.timer = NSTimer.scheduledTimerWithTimeInterval(interval, target:self,
                selector:"onTimer", userInfo:nil,
                repeats:true)
        } else {
            self.timer.invalidate()
            self.timer = nil
        }
    }
    
    func resetTimer() {
    
        if (self.timer != nil) {
            self.timer.invalidate()
            self.timer = nil
        }
        self.seconds = 0;
        self.timeLabel.text = "0.00"
    }
    
    func onTimer() {
        
        self.seconds = self.seconds + 1
        self.timeLabel.text = String(format: "%.2f", Double(self.seconds)*interval)
    }
    
    func initWatchConnection() {
    
        if (WCSession.isSupported()) {
            session = WCSession.defaultSession()
            session?.delegate = self
            session?.activateSession()
        }
    }
    
    func session(session: WCSession, didReceiveMessage message: [String : AnyObject],
                 replyHandler: ([String : AnyObject]) -> Void) {

        if let body:String = message["body"] as? String {
            if (body == "startstop") {
                dispatch_async(dispatch_get_main_queue(),{
                    self.startOrStopTimer()
                })
                replyHandler([ "answer" : "OK" ])
            }
            if (body == "reset") {
                dispatch_async(dispatch_get_main_queue(),{
                    self.resetTimer()
                })
                replyHandler([ "answer" : "OK" ])
            }
        }
    }
}



Теперь запускаем программу на часах и смартфоне, и все работает — нажатия кнопок на часах корректно запускают таймер, как и хотелось.

Отладка


Среда Xcode имеет в комплекте вполне функциональный симулятор, который выглядит примерно так.

3062c09295ed4e08aa96076bfd2f27ff.jpg

Почему-то среди пар «симулятор-часы» есть только iPhone 6 и iPhone 6 Plus, будут ли часы работать с другими моделями, мне неизвестно. Впрочем Apple Watch у меня все равно нет, как и iPhone 6/6Plus, так что проверить «вживую» увы, не на чем (это интересно, но не настолько, чтобы заплатить 30 тыс.р. за часы).

Размещение в App Store


Эту программу размещать в App Store мы разумеется, не будем. А для тех, кто захочет размещать там свои шедевры, отмечу кратко:

— необходимо добавить дополнительные иконки в Watch Kit App, они будут отображаться на часах
— необходимо добавить новые скриншоты и иконку в App Store
— необходимо добавить новые Apple ID для каждого компонента (App и Extention) и сгенерировать distribution-сертификаты для каждого компонента. Всего таким образом, получается 3 сертификата на программу.
— далее как обычно, Build-Archive, и полученный архив, содержащий все внутри, заливается в App Store.

Как-то так. Если кому-то интересно, можно будет продолжить. Как можно видеть, программирование для watchOS хоть и имеет некоторые особенности, но кардинально не отличается от создания обычной iOS-программы. Автор желает всем удачных экспериментов.

© Habrahabr.ru