Статьи

Ваше первое приложение WatchKit: взаимодействие с пользователем

В предыдущем уроке мы изучили основы разработки WatchKit. Мы создали проект в Xcode, добавили приложение WatchKit и создали базовый пользовательский интерфейс.

Пользовательский интерфейс нашего приложения WatchKit в настоящее время отображает статические данные. Если вы не живете в пустыне, это не очень полезно для погодных условий. В этом уроке мы собираемся заполнить пользовательский интерфейс данными и создать несколько действий.

Прежде чем заполнять пользовательский интерфейс данными, нам нужно внести небольшое изменение. В предыдущем уроке мы добавили экземпляр WKInterfaceDate в нижнюю группу для отображения текущего времени и даты. Однако было бы более полезно отобразить время и дату данных, которые мы отображаем. Причина этого изменения станет ясна через несколько минут.

Откройте Interface.storyboard , удалите экземпляр WKInterfaceDate в нижней группе и замените его на экземпляр WKInterfaceLabel . Установите для атрибута ширины метки значение « Относительно контейнера», а для выравнивания метки — по правому краю.

Добавление метки

Чтобы обновить пользовательский интерфейс динамическими данными, нам нужно создать несколько выходов в классе InterfaceController . Откройте раскадровку в главном редакторе и InterfaceController.swift в помощнике редактора справа. Выберите верхнюю метку в первой группе и перетащите Control из метки в класс InterfaceController чтобы создать розетку. Назовите locationLabel торговой locationLabel .

Повторите эти шаги для других меток, назвав их dateLabel и dateLabel соответственно. Вот как должен выглядеть класс InterfaceController когда вы закончите.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import WatchKit
import Foundation
 
class InterfaceController: WKInterfaceController {
    @IBOutlet weak var dateLabel: WKInterfaceLabel!
    @IBOutlet weak var locationLabel: WKInterfaceLabel!
    @IBOutlet weak var temperatureLabel: WKInterfaceLabel!
 
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    }
 
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
 
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
}

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

Чтобы помочь нам, Xcode заполнил класс InterfaceController тремя переопределенными методами. Важно понимать, когда каждый метод вызывается и для чего он может или должен использоваться.

В awakeWithContext(_:) вы устанавливаете и инициализируете контроллер интерфейса. Вам может быть интересно, чем он отличается от метода init . Метод awakeWithContext(_:) вызывается после инициализации контроллера интерфейса. Метод принимает один параметр, объект контекста, который позволяет интерфейсным контроллерам передавать информацию друг другу. Это рекомендуемый подход для передачи информации между сценами, то есть интерфейсными контроллерами.

Метод willActivate аналогичен viewWillAppear(_:) класса UIViewController . Метод willActivate вызывается до того, как пользовательский интерфейс контроллера интерфейса будет представлен пользователю. Он идеально подходит для настройки пользовательского интерфейса перед его представлением пользователю.

Метод didDeactivate является аналогом метода willActivate и вызывается при willActivate сцены контроллера интерфейса. Любой код очистки входит в этот метод. Этот метод похож на метод viewDidDisappear(_:) найденный в классе UIViewController .

Имея это в виду, мы можем начать загружать данные и обновлять пользовательский интерфейс нашего приложения WatchKit. Начнем с загрузки данных о погоде.

Вы можете подумать, что следующим шагом будет вызов API для службы погоды, но это не так. Если бы мы создавали приложение для iOS, вы были бы правы. Однако мы создаем приложение WatchKit.

Не рекомендуется делать сложные вызовы API для извлечения данных для заполнения пользовательского интерфейса приложения WatchKit. Хотя Apple явно не упоминает об этом в документации, инженер Apple упомянул эту неписаную лучшую практику на форумах разработчиков Apple .

Приложение WatchKit является частью приложения iOS, и это приложение iOS отвечает за выборку данных из удаленного сервера. Есть несколько подходов, которые мы можем использовать для этого, выборка фона — хороший выбор. Однако в этом уроке мы не будем фокусироваться на этом аспекте.

Вместо этого мы добавим фиктивные данные в комплект расширения WatchKit и загрузим их в метод awakeWithContext(_:) который мы обсуждали ранее.

Создайте пустой файл, выбрав New> File … в меню File . Выберите « Очистить» в разделе « iOS»> «Другое » и назовите файл weather.json . Дважды проверьте, что вы добавляете файл в расширение RainDrop WatchKit . Не забывайте эту маленькую, но важную деталь. Заполните файл следующими данными.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  «locations» : [
    {
      «location» : «Cupertino»,
      «temperature» : 24,
      «timestamp» : 1427429751
    },
    {
      «location» : «London»,
      «temperature» : 11,
      «timestamp» : 1427429751
    },
    {
      «location» : «Paris»,
      «temperature» : 9,
      «timestamp» : 1427429751
    },
    {
      «location» : «Brussels»,
      «temperature» : 11,
      «timestamp» : 1427429751
    }
  ]
}

Обмен данными между приложением iOS и приложением WatchKit является важной темой. Тем не менее, это руководство посвящено настройке и запуску вашего первого приложения WatchKit. В следующем уроке я сосредоточусь на обмене данными между iOS и приложением WatchKit.

Даже несмотря на то, что мы не будем рассказывать об обмене данными в этом руководстве, важно знать, что приложение iOS и расширение WatchKit не имеют общей песочницы. Обе цели имеют свою собственную изолированную программную среду, и это делает обмен данными менее тривиальным, чем кажется.

Для обмена данными между iOS и приложением WatchKit необходимо использовать группы приложений. Но это тема для будущего урока.

Swift — отличный язык, но некоторые задачи в Objective-C проще, чем в Swift. Например, обработка JSON является одной из таких задач. Чтобы упростить эту задачу, я решил использовать популярную библиотеку SwiftyJSON .

Загрузите репозиторий с GitHub , разархивируйте архив и добавьте SwiftyJSON.swift в группу RainDrop WatchKit Extension . Этот файл находится в папке Source архива. Дважды проверьте, что в SwiftyJSON.swift добавлена цель RainDrop WatchKit Extension .

Добавление библиотеки SwiftyJSON

Чтобы упростить работу с данными о погоде, хранящимися в weather.json , мы собираемся создать структуру с именем WeatherData . Выберите Создать> Файл … из в меню « Файл» выберите « Файл Swift» в разделе « iOS»> «Источник » и назовите файл « WeatherData» . Убедитесь, что файл добавлен в целевой объект RainDrop WatchKit Extension .

Реализация структуры WeatherData короткая и простая. Структура определяет три постоянных свойства, date , location и temperature .

1
2
3
4
5
6
7
import Foundation
 
struct WeatherData {
    let date: NSDate
    let location: String
    let temperature: Double
}

Поскольку значение температуры weather.json находится в градусах Цельсия, мы также реализуем вычисленное свойство fahrenheit для простого преобразования между градусами Цельсия и Фаренгейта.

1
2
3
var fahrentheit: Double {
    return temperature * (9 / 5) + 32
}

Мы также определяем два вспомогательных метода toCelciusString и toFahrenheitString чтобы упростить форматирование значений температуры. Разве вы не любите строковую интерполяцию Свифта?

1
2
3
4
5
6
7
func toCelciusString() -> String {
    return «\(temperature) °C»
}
 
func toFahrenheitString() -> String {
    return «\(fahrentheit) °F»
}

Как я уже сказал, реализация структуры WeatherData короткая и простая. Вот как должна выглядеть реализация.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import Foundation
 
struct WeatherData {
    let date: NSDate
    let location: String
    let temperature: Double
     
    var fahrentheit: Double {
        return temperature * (9 / 5) + 32
    }
     
    func toCelciusString() -> String {
        return «\(temperature) °C»
    }
     
    func toFahrenheitString() -> String {
        return «\(fahrentheit) °F»
    }
}

Прежде чем загружать данные из weather.json , нам нужно объявить свойство для хранения данных о погоде. Свойство weatherData имеет тип [WeatherData] и будет содержать содержимое weather.json в качестве экземпляров структуры WeatherData .

1
var weatherData: [WeatherData] = []

Для простоты использования мы также объявляем вычисляемое свойство weather , которое дает нам доступ к первому элементу массива weatherData . Это данные этого экземпляра WeatherData которые мы будем отображать в контроллере интерфейса. Можете ли вы догадаться, почему мы должны объявить свойство weather необязательным?

1
2
3
var weather: WeatherData?
    return weatherData.first
}

Мы загружаем данные из weather.json в awakeWithContext(_:) . Чтобы сохранить реализацию чистой, мы вызываем вспомогательный метод с именем loadWeatherData .

1
2
3
4
5
6
override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
     
    // Load Weather Data
    loadWeatherData()
}

Реализация loadWeatherData , пожалуй, самый сложный фрагмент кода, который мы увидим в этом руководстве. Как я уже сказал, анализ JSON не является тривиальным в Swift. К счастью, SwiftyJSON делает большую часть тяжелой работы за нас.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func loadWeatherData() {
    let path = NSBundle.mainBundle().pathForResource(«weather», ofType: «json»)
     
    if let path = path {
        let data = NSData(contentsOfFile: path)
         
        if let data = data {
            let weatherData = JSON(data: data)
            let locations = weatherData[«locations»].array
             
            if let locations = locations {
                for location in locations {
                    let timestamp = location[«timestamp»].double!
                    let date = NSDate(timeIntervalSinceReferenceDate: timestamp)
                     
                    let model = WeatherData(date: date, location: location[«location»].string!, temperature: location[«temperature»].double!)
                     
                    self.weatherData.append(model)
                }
            }
        }
    }
}

Мы получаем путь к weather.json и загружаем его содержимое как объект NSData . Мы используем SwiftyJSON для анализа JSON, передавая объект NSData . Мы получаем ссылку на массив для ключевых местоположений и зацикливаемся над каждым местоположением.

Мы нормализуем данные о погоде путем преобразования временной метки в экземпляр NSDate и инициализируем объект WeatherData . Наконец, мы добавляем объект weatherData массив weatherData .

Я надеюсь, что вы согласны с тем, что внедрение не так уж сложно. Поскольку Swift вынуждает нас сделать ряд проверок, реализация выглядит более сложной, чем она есть на самом деле.

Когда данные о погоде готовы к использованию, пришло время обновить пользовательский интерфейс. Как я объяснил ранее, обновление пользовательского интерфейса должно происходить в методе willActivate . Давайте посмотрим на реализацию этого метода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
     
    if let weather = self.weather {
        locationLabel.setText(weather.location)
         
        // Update Temperature Label
        self.updateTemperatureLabel()
         
        // Update Date Label
        self.updateDateLabel()
    }
}

После вызова метода суперкласса willActivate мы разворачиваем значение, сохраненное в свойстве weather . Чтобы обновить метку местоположения, мы вызываем setText , передавая значение, хранящееся в свойстве location объекта weather . Чтобы обновить метки температуры и даты, мы вызываем два вспомогательных метода. Я предпочитаю, чтобы метод willActivate коротким и лаконичным, и, что более важно, я не люблю повторяться.

Прежде чем мы рассмотрим эти вспомогательные методы, нам нужно знать, нужно ли отображать температуру в градусах Цельсия или Фаренгейта. Чтобы решить эту проблему, объявите свойство celcius типа Bool и установите для его начального значения значение true .

1
var celcius: Bool = true

Реализация updateTemperatureLabel проста для понимания. Мы безопасно распаковываем значение, сохраненное в weather и обновляем температурную метку на основе значения в celcius . Как вы можете видеть, два вспомогательных метода структуры WeatherData мы создали ранее, пригодятся.

1
2
3
4
5
6
7
8
9
func updateTemperatureLabel() {
    if let weather = self.weather {
        if self.celcius {
            temperatureLabel.setText(weather.toCelciusString())
        } else {
            temperatureLabel.setText(weather.toFahrenheitString())
        }
    }
}

Реализация updateDateLabel не сложна. Мы инициализируем экземпляр NSDateFormatter , устанавливаем его свойство dateFormat и конвертируем дату объекта weather , вызывая stringFromDate(_:) dateFormatter объекта dateFormatter . Это значение используется для обновления метки даты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func updateDateLabel() {
    var date: NSDate = NSDate()
     
    // Initialize Date Formatter
    let dateFormattter = NSDateFormatter()
     
    // Configure Date Formatter
    dateFormattter.dateFormat = «d/MM HH:mm»
     
    if let weather = self.weather {
        date = weather.date
    }
     
    // Update Date Label
    dateLabel.setText(dateFormattter.stringFromDate(date))
}

Создайте и запустите приложение, чтобы увидеть результат. Теперь пользовательский интерфейс должен быть заполнен данными из weather.json .

Готовый пользовательский интерфейс RainDrop

Это выглядит хорошо. Но разве не было бы замечательно, если бы мы добавили поддержку и Цельсию, и Фаренгейту? Это легко сделать, так как мы уже заложили большую часть основы.

Если пользовательское усилие касается пользовательского интерфейса контроллера пользовательского интерфейса, отображается меню. Конечно, это работает только при наличии меню. Посмотрим, как это работает.

Откройте Interface.storyboard и добавьте меню к контроллеру интерфейса в схеме документа слева. По умолчанию меню имеет один пункт меню. Нам нужны два пункта меню, поэтому добавьте еще один пункт меню в меню.

Добавление меню с двумя пунктами меню

Обратите внимание, что меню и его элементы не отображаются в пользовательском интерфейсе. Это не проблема, так как мы не можем настроить макет меню. Что мы можем изменить, так это текст пункта меню и его изображение. Вы лучше поймете, что это значит, когда представим меню.

Выберите верхний пункт меню, откройте инспектор атрибутов , установите для параметра « Заголовок» значение « Цельсий» и « Изображение для подтверждения» . Выберите нижний пункт меню и установите для заголовка значение Фаренгейт, а для изображенияПринять .

Настройка пункта меню

Затем откройте InterfaceController.swift в помощнике редактора справа. toCelcius Control», перетащите элемент верхнего меню в InterfaceController.swift и создайте действие с именем toCelcius . Повторите этот шаг для нижнего пункта меню, создав действие с именем toFahrenheit .

Реализация этих действий коротка. В toCelcius мы проверяем, установлено ли для свойства celcius значение false , и, если оно установлено, мы устанавливаем для свойства значение true . В toFahrenheit мы проверяем, установлено ли для свойства celcius значение true , и, если оно установлено, мы устанавливаем для свойства значение false .

01
02
03
04
05
06
07
08
09
10
11
@IBAction func toCelcius() {
    if !self.celcius {
        self.celcius = true
    }
}
 
@IBAction func toFahrenheit() {
    if self.celcius {
        self.celcius = false
    }
}

Если значение в celcius изменяется, нам нужно обновить пользовательский интерфейс. Что может быть лучше для реализации этого путем реализации наблюдателя свойства в свойстве celcius . Нам нужно только реализовать наблюдателя свойства didSet .

1
2
3
4
5
6
7
var celcius: Bool = true {
    didSet {
        if celcius != oldValue {
            updateTemperatureLabel()
        }
    }
}

Единственная деталь, о которой стоит упомянуть, это то, что пользовательский интерфейс обновляется только в том случае, если значение celcius изменилось. Обновление пользовательского интерфейса так же просто, как вызов updateTemperatureLabel . Создайте и запустите приложение WatchKit в iOS Simulator, чтобы протестировать меню.

Стоит отметить, что iOS Simulator имитирует отзывчивость физического устройства. Что это обозначает? Помните, что расширение WatchKit работает на iPhone, а приложение WatchKit — на Apple Watch. Когда пользователь касается элемента меню, сенсорное событие отправляется через соединение Bluetooth на iPhone. Расширение WatchKit обрабатывает событие и отправляет все обновления обратно в Apple Watch. Это общение довольно быстрое, но не такое быстрое, как если бы расширение и приложение запускались на одном устройстве. Эта короткая задержка имитируется iOS Simulator, чтобы помочь разработчикам получить представление о производительности.

После того, как вы освоили архитектуру приложения WatchKit, становится намного легче понять возможности и ограничения приложений WatchKit первого поколения. В этом уроке мы рассмотрели только основы разработки WatchKit. Существует гораздо больше, чтобы открыть и исследовать. Будьте на связи.