В iOS 8 было представлено много полезных улучшений, одной из самых мощных новых функций является концепция расширений. Расширения предоставляют несколько способов обмена функциями и данными с iOS или другими приложениями из вашего собственного приложения.
Расширения в iOS существуют во многих различных формах. Каждый имеет дело с определенной частью системы. Здесь мы сосредоточимся на расширении Today, иначе известном как виджет. Виджеты отображаются в центре уведомлений в представлении «Сегодня». Прежде чем мы создадим расширение, важно понять концепции, стоящие за ними.
Что такое расширения?
Расширения позволяют вам передавать пользовательские функции или контент из вашего приложения в другие области iOS. Все расширения созданы, чтобы служить центральной цели, они обычно не имеют различных компонентов, а создаются для облегчения определенной задачи.
Многие области операционной системы поддерживают виджеты. Они называются точками расширения . Функциональность, которую вы хотите предоставить, определит, на какую точку расширения вы нацелены В iOS 8 есть семь точек расширения:
- Сегодня (iOS & OS X) — смотрите быстрые обновления в представлении Сегодня.
- Поделиться (iOS & OS X) — делиться контентом с другими или публиковать его на веб-сайте.
- Действие (iOS & OS X) — редактируйте или просматривайте контент, предоставленный хост-приложением.
- Редактирование фотографий (iOS) — отредактируйте фотографию или видео с помощью приложения «Фотографии».
- Поставщик документов (iOS) — Доступ и редактирование хранилища файлов.
- Настраиваемая клавиатура (iOS). Предоставьте настроенную системную клавиатуру.
- Finder Sync (OS X) — Показать информацию о состоянии синхронизации файлов в Finder.
Внутри Xcode каждый шаблон расширения настроен для одной из перечисленных выше точек расширения. Не существует понятия «общего» расширения, которое будет работать с более чем одной точкой расширения.
Каждое расширение существует внутри приложения контейнера как отдельный двоичный файл. Они добавляются в существующий проект как новая цель. Это означает, что вы не выпускаете расширения по отдельности. Вместо этого они выпускаются вместе со своим контейнерным приложением. Одно приложение может содержать несколько разных расширений.
Теперь, когда мы хорошо понимаем, что такое расширения, давайте сосредоточимся на расширении Today, которое мы собираемся создать.
Соображения по поводу дизайна виджетов
Есть определенные правила, которые вы должны соблюдать при создании виджета. Виджеты должны быть сфокусированными, отзывчивыми и содержать краткую информацию. Это означает, что если вы создаете виджет для спортивного приложения, он может отображать счет для конкретной игры. Он не будет включать в себя пошаговый процесс, такой как вход, просмотр всех результатов в нескольких видах спорта и т. Д.
Помимо пользовательского опыта, есть и технические ограничения. Например, виджеты не могут отображать клавиатуру. В представлении «Сегодня» также используются размытые визуальные эффекты, поэтому рекомендуется сохранять четкость фона. Основной жест пользователя в представлении «Сегодня» — смахивание вверх или вниз, поэтому использование прокрутки также не рекомендуется.
Что будет делать наш виджет?
Виджет, который мы собираемся создать, будет отображать информацию о геолокации на основе вашего IP-адреса с использованием API, предоставленного Telize . Когда iOS пытается обновить виджет, в API отправляется запрос данных, если они еще не были получены. JSON будет сохранен в NSDictionary
.
Когда данные анализируются, отображается случайное значение из вашего местоположения, такое как широта, долгота, код страны и т. Д. Также будет кнопка для обновления метки для отображения нового случайного значения.
Apple рекомендует использовать виджеты с автоматическим макетом. Недавно iOS 8 представила адаптивный дизайн, который побуждает разработчиков создавать свои представления так, чтобы они могли быть правильно представлены на любом устройстве. Это гарантирует, что ваш виджет способен обеспечить исключительный пользовательский опыт, будь то на iPad, iPhone 4 или iPhone 6 Plus. SitePoint недавно опубликовал статью об авто макете , прочитайте, если вы хотите узнать больше.
Вы можете найти окончательный проект, который мы создаем на GitHub .
Создание новой цели
Начните с открытия Xcode и создания нового проекта, используя шаблон приложения с одним представлением. Если у вас уже есть приложение, к которому вы хотите добавить этот виджет, вы можете пропустить этот шаг. Добавьте новую цель, выбрав « Файл»> «Создать»> «Цель», а затем выберите « Расширение сегодня», указанное в разделе « Расширение приложения» в разделе « Расширение iOS» .
Заполните параметры для названия продукта и организационного идентификатора для цели. Если вы хотите следовать коду для этого урока, обязательно выберите swift в качестве языка программирования. Напомним, что расширения размещены внутри приложений. Вот почему идентификатор пакета основан на том, который уже существует для проекта. Нажмите «Готово», чтобы добавить расширение.
Интерфейс пользователя
Убедитесь, что вы открыли папку Widget, прежде чем продолжить. По умолчанию шаблон виджета добавляет контроллер представления, раскадровку и файл plist. Если вы откроете MainInterface.storyboard , вы заметите, что для пользовательского интерфейса предусмотрена одна метка. Для нашего виджета нам также нужно добавить кнопку.
Начните с двойного щелчка на ярлыке, введите «Загрузка…» и нажмите ввод. Затем откройте инспектор размера . Установите для Width значение 254. Метка станет оранжевой, чтобы указать, что кадр изменился и что текущие ограничения недействительны. Мы исправим это позже.
Затем перетащите кнопку из библиотеки объектов и поместите ее к правой стороне метки. Откройте инспектор Атрибутов и установите для параметра « Тип» информационный индикатор и снимите флажок « Показывать касанием» . В инспекторе размера установите кнопки X на 266 и Y на 8.
Создать IBOutlets и IBAction
Нажмите кнопку редактора Assistant в правом верхнем углу Xcode. Код TodayViewController
должен отображаться в правой части Xcode. Это позволит легко подключить розетки внутри контроллера вида.
Ctrl + Перетащите с метки в верхнюю часть TodayViewController
и отпустите. В lblGeoData
строке введите lblGeoData
в качестве имени . Нажмите подключиться.
Сделайте то же самое для кнопки, но для имени назовите ее btnRefresh
. Повторите этот процесс еще раз, но отпустите кнопку мыши в нижней части контроллера вида. Установите для типа подключения значение « Действие», а для « Имя» — значение btnRefreshData
.
С выбранной меткой внизу холста раскадровки нажмите кнопку «Устранить проблемы с автоматической компоновкой» и нажмите « Очистить ограничения» в разделе « Все представления ».
Убедитесь, что метка все еще выбрана. Используя кнопку « Закрепить» в нижней части холста раскадровки, добавьте ограничения, показанные на рисунке ниже, и нажмите « Добавить 5 ограничений» .
Нам нужно отредактировать некоторые приоритеты для только что добавленных ограничений. В инспекторе размера вы увидите все ограничения, примененные к метке.
Откройте ограничение ширины и измените отношение на « Больше или равно» . Для ограничений сверху и снизу уменьшите их приоритеты до 999
.
Выберите кнопку, которую мы добавили рядом с меткой, и снова откройте всплывающее окно « Pin» . Добавьте следующие ограничения и нажмите Добавить 4 ограничения :
Вернитесь к стандартному представлению редактора , нажав кнопку рядом с помощником редактора.
NCWidgetProviding Protocol
Теперь, когда пользовательский интерфейс полностью настроен, откройте файл TodayViewController.swift
. По своей сути виджеты — это не больше, чем один контроллер представления. Приведенный здесь соответствует важному протоколу, NCWidgetProviding
. Этот протокол предоставляет функции для настройки внешнего вида и поведения виджета.
NCWidgetProviding
есть две функции делегата. Первый, widgetPerformUpdateWithCompletionhandler:
требуется реализовать. Вам не нужно вызывать эту функцию вручную. Если мы укажем, что в нем есть новые данные, снимок будет кэширован, а затем использован для обновления дисплея.
Другая функция делегата, определенная в протоколе, является widgetMarginInsetsForProposedMarginInsets:
В отличие от последней функции, это необязательно для реализации. По умолчанию виджеты создаются с вложенным левым полем. Если вы посмотрите на предоставленные Apple виджеты в представлении «Сегодня», например «Календарь», вы заметите пространство слева. Если вы хотите UIEdgeInsets
свой виджет, вы можете вернуть соответствующие UIEdgeInsets
здесь.
Получение данных
Нам нужно написать код, чтобы использовать API для отображения данных в нашем виджете. Сначала создайте свойство для поддержания статуса обновления виджета. Добавьте следующее свойство в TodayViewController.swift
под существующими IBOutlet
для нашего ярлыка:
var updateResult:NCUpdateResult = NCUpdateResult.NoData
Мы будем использовать это свойство, чтобы сообщать системе о необходимости обновления нашего пользовательского интерфейса после получения новых данных из API.
Создайте другое свойство с именем geoInfoDictionary
типа NSDictionary
. Это позволит кэшировать данные, поэтому новый запрос к API не требуется для отображения новой информации в метке.
Выше viewDidLoad:
ваш код должен выглядеть следующим образом:
@IBOutlet var btnRefresh: UIButton! @IBOutlet var lblGeoData: UILabel! var updateResult:NCUpdateResult = NCUpdateResult.NoData var geoInfoDictionary:NSDictionary?
На данный момент пришло время вызвать API и получить данные о географическом местоположении о вашем текущем IP-адресе. Добавьте этот код в контроллер представления (не беспокойтесь о setLabelText()
, мы добавим это далее):
//MARK: Update Logic - Data Retrieval func updateWidget() { if self.geoInfoDictionary == nil { self.lblGeoData.text = "Refreshing..." let urlPath: String = "http://www.telize.com/geoip?" let url: NSURL = NSURL(string: urlPath)! let request: NSURLRequest = NSURLRequest(URL: url) let queue:NSOperationQueue = NSOperationQueue() NSURLConnection.sendAsynchronousRequest(request, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in if(error != nil) { self.updateResult = NCUpdateResult.Failed self.lblGeoData.text = "An error occurred while retrieving data" } else { let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary self.geoInfoDictionary = jsonResult } self.setLabelText() }) } else { self.setLabelText() } }
Над объявлением функции мы видим нечто похожее на комментарий. Это похоже на прагма в Objective-C. Использование //MARK:
помогает разбить код на логические разделы, которые можно просматривать внутри панели перехода.
В функции updateWidget()
мы используем API. Проверка выполняется сначала, чтобы увидеть, был ли запрос успешным. Если это не так, объект NSError
будет инициализирован. Если это происходит, updateResult
устанавливается на сбой .
Если ошибка равна нулю , данные были получены из ответа. geoInfoDictionary
заполняется данными о географическом местоположении.
Затем создайте эту функцию прямо под updateWidget():
func setLabelText() { if let geoDictionary = self.geoInfoDictionary { let randomKeyIndex = Int(arc4random_uniform(UInt32(geoDictionary.count))) let randomKey = geoDictionary.allKeys[randomKeyIndex] as String let keyValue = geoDictionary[randomKey] as String let lblText = "\(randomKey) - \(keyValue)" if(self.lblGeoData.text != lblText) { self.updateResult = NCUpdateResult.NewData self.lblGeoData.text = lblText } } else { self.updateResult = NCUpdateResult.NoData } }
Эта функция обновляет метку, когда она обновляется нажатием кнопки или когда JSON возвращается в первый раз. Случайное значение выбирается из словаря географического местоположения и отображается на метке, если оно отличается от того, что уже отображается.
Кнопка, которую мы добавили ранее, обновит метку другим значением. Завершите функцию btnRefreshData
сейчас:
@IBAction func btnRefreshData(sender: UIButton) { self.geoInfoDictionary == nil ? self.updateWidget() : self.setLabelText() }
Когда кнопка нажата, она проверяет, есть ли у нас ответ от API. Если мы это сделаем, нет причин снова попадать на сервер. Все данные о географическом местоположении сохраняются в свойстве geoInfoDictionary
. Несмотря на это, лейбл обновляется с чем-то новым.
Чтобы закончить, реализуйте viewDidAppear:
вот так:
override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.updateWidget() }
Вызов обработчика завершения
Виджет должен обновляться, когда он находится за пределами экрана. Это постоянно обновляет его. Мы обязаны сообщить системе, должно ли это произойти. Измените функцию протокола widgetPerformUpdateWithCompletionHandler(completionHandler:
попытаться показать свежие данные, а затем выполните обработчик завершения:
//MARK: NCWidgetProviding Functions func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) { self.updateWidget() completionHandler(self.updateResult) }
Если вы создаете виджет, подобный этому, было бы разумно кэшировать кавычки, используя NSUserDefaults
. Права группы приложений могут быть созданы для виджета и содержащего его приложения, если вы хотите поделиться цитатой за пределами виджета. Это позволит NSUserDefaults
синхронизировать между обоими целями, используя initWithSuiteName:
initializer.
Установка полей маржи
Если бы вы сейчас собрали и запустили виджет, поля могли бы показаться немного неприличными. Чтобы исправить это, установите некоторые пользовательские значения вставки внутри widgetMarginInsetsForProposedMarginInsets:
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { var newMargins = defaultMarginInsets newMargins.right = 10 newMargins.bottom = 5 return newMargins }
В зависимости от типа создаваемого виджета вам может даже не понадобиться это реализовывать. Помните, что это дополнительная функция внутри протокола NCWidgetProviding
.
Теперь, когда вы добавили этот код, весь ваш контроллер представления должен выглядеть так:
import UIKit import NotificationCenter class TodayViewController: UIViewController, NCWidgetProviding { //MARK: Properties @IBOutlet var btnRefresh: UIButton! @IBOutlet var lblGeoData: UILabel! var updateResult:NCUpdateResult = NCUpdateResult.NoData var geoInfoDictionary:NSDictionary? //MARK: View Lifecycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view from its nib. } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.updateWidget() } //MARK: NCWidget Providing func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) { self.updateWidget() completionHandler(self.updateResult) } func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { var newMargins = defaultMarginInsets newMargins.right = 10 newMargins.bottom = 5 return newMargins } //MARK: Update Logic - Data Retrieval func updateWidget() { if self.geoInfoDictionary == nil { self.lblGeoData.text = "Refreshing..." let urlPath: String = "http://www.telize.com/geoip?" let url: NSURL = NSURL(string: urlPath)! let request: NSURLRequest = NSURLRequest(URL: url) let queue:NSOperationQueue = NSOperationQueue() NSURLConnection.sendAsynchronousRequest(request, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in if(error != nil) { self.updateResult = NCUpdateResult.Failed self.lblGeoData.text = "An error occurred while retrieving data" } else { let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary self.geoInfoDictionary = jsonResult } self.setLabelText() }) } else { self.setLabelText() } } func setLabelText() { if let geoDictionary = self.geoInfoDictionary { let randomKeyIndex = Int(arc4random_uniform(UInt32(geoDictionary.count))) let randomKey = geoDictionary.allKeys[randomKeyIndex] as String let keyValue = geoDictionary[randomKey] as String let lblText = "\(randomKey) - \(keyValue)" if(self.lblGeoData.text != lblText) { self.updateResult = NCUpdateResult.NewData self.lblGeoData.text = lblText } } else { self.updateResult = NCUpdateResult.NoData } } @IBAction func btnRefreshData(sender: AnyObject) { self.geoInfoDictionary == nil ? self.updateWidget() : self.setLabelText() } }
Запуск виджета
Создайте и запустите виджет, нажав Ctrl + R. Поскольку мы использовали автоматическую разметку, не важно, какой симулятор вы выберете. Вам будет предложено выбрать приложение для запуска. Выберите Сегодня . Когда симулятор запустится, вы увидите экран Today. Если виджет не появляется, нажмите «Изменить» в нижней части экрана и добавьте его.
Через 5–10 секунд ответ от API возвращается и отображается некоторая случайная информация о геолокации. Нажмите кнопку несколько раз, чтобы просмотреть некоторые данные.
Вывод
Расширения открывают множество интересных возможностей для разработчиков. Виджеты оказались одной из самых популярных новых функций iOS среди пользователей. Если у вас есть существующие приложения, опубликованные в магазине приложений, спросите себя, могут ли они воспользоваться виджетом. Не нужно много кода, чтобы запустить и запустить виджет, как только вы поймете принципы NCWidgetProviding
.
Не забывайте держать их прямыми, целенаправленными и ориентированными на производительность. Краткие сведения лучше всего подходят для экрана «Сегодня». Обратите особое внимание на то, как работают виджеты Apple. С основами, описанными в этом руководстве, у вас есть все, что нужно для начала работы с вашим собственным виджетом.