iOS8 — Widgets

в 12:20, , рубрики: Без рубрики

iOS8 — Widgets
С выходом iOS 8 у разработчиков появилась возможность создавать свои виджеты для экрана Today. Пока еще API окончательно не утряслось, есть Known Issue и много неописанных в документации моментов. Но если вы все же хотите сделать свой виджет, то прошу под кат (внимание, в примерах используется Swift).

Extensions

В iOS появилась новая концепция — расширения. Расширения позволяют сделать доступной какую-то часть контента и функциональности вне приложения.

Часть системы, которая поддерживает расширения, называется extension point. Для iOS доступны следующие extension point:

  • Today (Notification Center) — быстро выполнить какое-то действие или получить информацию через экран Today в Notification Center
  • Share — поделиться контентом с друзьями или в ленте на каком-нибудь сайте
  • Action — просматривать или управлять контентом внутри контекста другого приложения
  • Photo Editing — редактировать фото или видео внутри приложения Photos
  • Storage Provider — выбрать документ из набора документов доступных текущему приложению
  • Custom Keyboard — заменить родную клавиатуру iOS своей для использования во всех приложения

Расширения могут распространяться только в контейнере расширений, которым служит бандл обычного приложения. Один контейнер может содержать несколько расширений.

Следует особо отметить, что расширения являются особым видом бинарных файлов. Это не приложения!
К сожалению, расширения не поддерживают App to App IPC (пайпы, сокеты, ...), и поэтому нужно использовать всем знакомый [UIApplication openURL:] (сейчас он не работает для расширений, см. Known Issue) или, например, App Group.

Каждое расширение запускается в отдельном процессе. Таким образом, одинаковые расширения в контексте разных приложений являются разными процессами, и можно не волноваться о проблемах синхронизации.

Документация по расширениям лежит здесь.

Widgets

Виджетами называют расширения, которые отображают информацию в Notification Center на экране Today и, следовательно, призваны показывать ту информацию, которая важна в текущий момент. Когда пользователь открывает Today, то он ожидает, что интересующая его информация будет мгновенно доступна.

Виджет становится доступным после того, как пользователь установит приложение, содержащее виджет (сейчас бывает, что виджет не устанавливается после первого запуска приложения, все-таки это еще бета). Чтобы добавить виджет, надо открыть экран Today в Notification Center, нажать кнопку Edit и добавить нужный виджет.

Связь между контейнером и виджетом осуществляется через NotificationCenter.framework.

По сути, виджет — это UIViewController, который хорошо знаком любому iOS-программисту. Соответственно, при создании виджетов можно использовать накопленные ранее знания. Например, если надо выполнить какое-то действие перед отображением виджета, то следует переопределить viewWillAppear и т.д.

Чтобы виджет всегда выглядел актуальным, iOS иногда делает снапшоты виджета. Когда виджет снова становится видимым, сначала показывается последний снапшот, а лишь потом настоящее окно виджета. Чтобы виджет обновил свое состояние перед снапшотом, используется протокол NCWidgetProviding.

protocol NCWidgetProviding : NSObjectProtocol {
    
// Called to allow the client to update its state prior to a snapshot being taken, or possibly other operations.
// Clients should call the argument block when the work is complete, passing the appropriate 'NCUpdateResult'.
    @optional func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!)
    
// Clients wishing to customize the default margin insets can return their preferred values.
// Clients that choose not to implement this method will receive the default margin insets.
    @optional func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets
}

Когда у виджета вызывается widgetPerformUpdateWithCompletionHandler, он должен обновить свое окно и после вызвать блок completionHandler с аргументом, равным одной из следующих констант:

  • NCUpdateResultNewData — новый контент требует обновления окна
  • NCUpdateResultNoData — виджету не нужно обновление
  • NCUpdateResultFailed — произошла ошибка в процессе обновления

Так как пользователи ждут мгновенной реакции от Notification Center, и система выполняет снапшоты, то виджет просто обязан хранить свое предыдущее состояние, т. е. кэшировать необходимые ему для работы данные.

Notification Center определяет ширину виджета, при этом виджет сам определяет свою высоту. Чтобы определить высоту, виджет может использовать Auto Layout или свойство preferedContentSize принадлежащее экзепляру UIViewController.

override func viewDidLoad() {
    super.viewDidLoad()     
    self.preferredContentSize.height = 350
 }

Получается, что к виджетам есть следующие требования:

  • гарантировать, что отображаемый контент актуален
  • должным образом реагировать на действия пользователя
  • потреблять минимально возможное количество ресурсов (iOS может убить виджет, если он будет потреблять много памяти)

Обратите внимание, что UI для виджетов имеет следующие ограничения:

  • нельзя показывать клавиатуру
  • нельзя использовать контролы, которые работают с жестами (например UIDatePicker)
  • пока что нельзя отобразить карты (сейчас этот пункт значиться в Known Issue — Mapviews do not load tiles in widgets.)

Так как клавиатура недоступна, пользователь должен иметь возможность конфигурировать виджет в приложении-контейнере.

Взаимодействие с виджетом

Для обновления информации, отображаемой виджетом, есть класс NCWidgetController.
Экземпляр этого класса имеет единственный метод setHasContent:forWidgetWithBundleIdentifier:, который посылает виджету сообщение о том, что он должен обновить информацию.
Используется следующим образом:

NCWidgetController.widgetController().setHasContent(true,
 forWidgetWithBundleIdentifier: "com.e-legion.Traffic.Widget")

Этот класс может использоваться из виджета и приложения-контейнера.

Обменным данными с приложением-контейнером

Для общения с контейнером используется объект NSExtensionContext доступный через свойство extensionContext, принадлежащее UIViewController.

class NSExtensionContext : NSObject {
    
    // The list of input NSExtensionItems associated with the context. If the context has no input items, this array will be empty.
    var inputItems: AnyObject[]! { get }
    
    // Signals the host to complete the app extension request with the supplied result items. The completion handler optionally contains any work which the extension may need to perform after the request has been completed, as a background-priority task. The `expired` parameter will be YES if the system decides to prematurely terminate a previous non-expiration invocation of the completionHandler. Note: calling this method will eventually dismiss the associated view controller.
    func completeRequestReturningItems(items: AnyObject[]!, completionHandler: ((Bool) -> Void)!)
    
    // Signals the host to cancel the app extension request, with the supplied error, which should be non-nil. The userInfo of the NSError will contain a key NSExtensionItemsAndErrorsKey which will have as its value a dictionary of NSExtensionItems and associated NSError instances.
    func cancelRequestWithError(error: NSError!)
    
    // Asks the host to open an URL on the extension's behalf
    func openURL(URL: NSURL!, completionHandler: ((Bool) -> Void)!)
}

Т.е чтобы открыть приложение-контейнер из виджета, контейнер должен зарегистрировать схему (например «traffic://»), и в код виджета надо добавить

self.extensionContext.openURL(NSURL(string: "traffic://"), completionHandler: nil)

.
По умолчанию, система безопасности iOS запрещает обмен данными между приложением-контейнером и расширением. Чтобы включить обмен данными, нужно добавить таргет контейнера и расширения в одну App Group.
iOS8 — Widgets
В результате, файл entitlements будет иметь следующее содержимое:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.application-groups</key>
	<array>
		<string>group.96GT47C53G.traffic</string>
	</array>
</dict>
</plist>

Обратите внимание на 96GT47C53G. Это Development Team ID. Посмотреть его можно в своем профиле. Для запуска на симуляторе можно использовать любое значение, например, group.traffic,…

Теперь при помощи метода containerURLForSecurityApplicationGroupIdentifier можно получить путь до общей папки и хранить там общие для приложений данные.

NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.96GT47C53G.traffic")

Пример

Итак, попробуем создать виджет, который будет показывать карту с пробками. Информацию о пробках будем получать в виде картинки при помощи API Яндекс.Карт.

Пример ссылки для получения картинки: http://static-maps.yandex.ru/1.x/?ll=30.35,59.9690273&spn=0.01,0.2&size=300,250&l=map,trf
Собственно, lat и lon — это центр карты, а spn — это протяженность области показа карты в градусах.

Исходный код проекта доступен на GitHub.

Создаем приложение-контейнер

Задача контейнера — дать пользователю возможность сконфигурировать виджет, а именно выбрать область на карте. Таким образом, приложение будет содержать MapView для выбора области и кнопку «Set frame», которая передаст эту область виджету.
iOS8 — Widgets

Приложение должно дать пользователю возможность выбрать параметры lat, lon, spn и передать их виджету. Этим занимается следующий код:

@IBAction func updateWidgetButtonTapped(sender : AnyObject) {
    var dict : NSMutableDictionary = NSMutableDictionary()
    dict["spn"] = self.mapView.region.span.latitudeDelta
    dict["lat"] = self.mapView.region.center.latitude
    dict["lon"] = self.mapView.region.center.longitude
      
    var dictUrl : NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.96GT47C53G.traffic").URLByAppendingPathComponent("settings.dict")
    dict.writeToFile(dictUrl.path, atomically: true)
 
    NCWidgetController.widgetController().setHasContent(true, forWidgetWithBundleIdentifier: "com.e-legion.Traffic.Widget")
}

Добавляем виджет

Добавление виджета сводится к добавлению нового таргета.
Жмем File->New->Target и выбираем iOS->Application Extension->Today Extension.

iOS8 — Widgets

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

init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
}

Обратите внимание на файл Info.plist из шаблона. Он содержит ключ NSExtension, в котором определены некоторые параметры виджета.

<key>NSExtension</key>
	<dict>
		<key>NSExtensionMainStoryboard</key>
		<string>MainInterface</string>
		<key>NSExtensionPointIdentifier</key>
		<string>com.apple.widget-extension</string>
	</dict>

NSExtensionMainStoryboard хранит название сториборда, в котором хранится контроллер для виджета. Контроллер можно указать явно, заменив ключ NSExtensionMainStoryboard на NSExtensionPrincipalClass и используя в качестве значения имя контроллера.

У каждого виджета есть небольшой сдвиг слева. Если вы хотите от него избавиться, то нужно возвращать нужный UIEdgeInsets в методе widgetMarginInsetsForProposedMarginInsets.

 func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
    return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
 }

Код виджета достаточно прост. Есть метод updateMap, который выполняет обновление карты. Обновление происходит при начале отображения виджета (viewWillLoad), нажатии на кнопку и вызове widgetPerformUpdateWithCompletionHandler. Данные об отображаемой области виджет получает через containerURLForSecurityApplicationGroupIdentifier.

Заключение

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

Автор: Fanruten

Источник

Поделиться

* - обязательные к заполнению поля