Статьи

Создание расширения Today для iOS 8

В 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: помогает разбить код на логические разделы, которые можно просматривать внутри панели перехода.

jumpBarMarks

В функции 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. С основами, описанными в этом руководстве, у вас есть все, что нужно для начала работы с вашим собственным виджетом.