Хабрахабр

Все, что нужно знать об iOS App Extensions

Приложения могут отображаться в виде виджета в Центре Уведомлений, предлагать свои фильтры для фотографий в Photos, отображать новую системную клавиатуру и многое другое. App Extensions появились в iOS 8 и сделали систему более гибкой, мощной и доступной для пользователей. Об особенностях работы App Extensions и пойдёт речь ниже.
Apple всегда стремилась тщательно изолировать приложения друг от друга. При этом сохранилась безопасность пользовательских данных и системы. Каждому приложению отводится отдельное место в файловой системе с ограниченным доступом. Это лучший способ обеспечить безопасность пользователей и защитить их данные. Таким образом, часть его функциональности будет доступной для пользователей, когда они взаимодействуют с другими приложениями или системой. Появление App Extensions позволило взаимодействовать с приложением без его запуска или показа на экране.

Сами по себе они не могут быть опубликованы в App Store, только вместе с Containing App. App Extensions представляют собой исполняемые файлы, которые запускаются независимо от содержащего их приложения – Containing App. Например: Custom Keyboard Extensions предназначены для замены стандартной клавиатуры, а Photo Editing Extensions — для редактирования фотографий в Photos. Все App Extensions выполняют одну определённую задачу и привязаны только к одной области iOS в зависимости от своего типа. Всего сейчас существует 25 типов App Extensions.

Приложение, которое пользователь использует для запуска App Extension, называется Host App. Host App запускает жизненный цикл App Extension, отправляя ему запрос в ответ на действие пользователя:

  • Пользователь выбирает App Extension через Host App.
  • Host App отправляет запрос App Extension.
  • iOS запускает App Extension в контексте Host App и устанавливает между ними канал связи.
  • Пользователь выполняет действие в App Extension.
  • App Extension завершает запрос от Host App, выполняя задачу, или запускает фоновый процесс для ее выполнения; по завершении задачи результат может быть возвращен Host App.
  • Как только App Extension выполнит свой код, система завершает этот App Extension.

Например, когда делимся фотографией из Photos с помощью Facebook Share Extension, Facebook является Containing App, а Photos – Host App. В этом случае Photos запускает жизненный цикл Facebook Share Extension, когда пользователь выбирает его в меню «Поделиться»:

  • Containing App – Host App
    Не взаимодействуют друг с другом.
  • App Extension – Host App
    Взаимодействуют с использованием IPC.
  • App Extension – Containing App
    Непрямое взаимодействие. Для обмена данными используются App Groups, а для общего кода – Embedded Frameworks. Запустить Containing App из App Extension можно с помощью URL Schemes.

Общий код: динамические фреймворки

Если Containing App и App Extension используют один и тот же код, его стоит поместить в динамический фреймворк.

Хорошим решением будет создать для этих фильтров динамический фреймворк. Например, с приложением для редактирования пользовательских фотографий может быть связано Photo Editing Extension, использующее некоторые фильтры из Containing App.

Для этого добавляем новый Target и выбираем Cocoa Touch Framework:

Указываем имя (например, ImageFilters), и в панели навигатора можно увидеть новую папку с названием созданного фреймворка:

Необходимо убедиться, что фреймворк не использует API, недоступные для App Extensions:

  • Shared из UIApplication.
  • API, помеченные макросами недоступности.
  • Камера и микрофон (кроме iMessage Extension).
  • Выполнение длительных фоновых задач (особенности этого ограничения различаются в зависимости от типа App Extension).
  • Получение данных с помощью AirDrop.

Использование чего-либо из этого списка в App Extensions приведёт к его отклонению при публикации в App Store.

В настройках фреймворка в General необходимо поставить галочку напротив «Allow app extension API only»:

Везде, где нужно использовать фреймворк, делаем import: В коде фреймворка все классы, методы и свойства, используемые в Containing App и App Extensions, должны быть public.

import ImageFilters

Обмен данными: App Groups

Containing App и App Extension имеют собственные ограниченные участки файловой системы, и только они имеют к ним доступ. Чтобы Containing App и App Extension имели общий контейнер с доступом на чтение и запись, нужно создать для них App Group.

App Group создаётся в Apple Developer Portal:

В правом верхнем углу нажимаем «+», в появившемся окне вводим необходимые данные:

Далее Continue -> Register -> Done.

В настройках Containing App переходим на вкладку Capabilities, активируем App Groups и выбираем созданную группу:

Аналогично для App Extension:

Далее поговорим о том, как осуществлять в него чтение и запись. Теперь Containing App и App Extension имеют общий контейнер.

UserDefaults

Для обмена небольшим количеством данных удобно использовать UserDefaults, нужно лишь указать название App Group:

let sharedDefaults = UserDefaults(suiteName: "group.com.maxial.onemoreapp")

NSFileCoordinator и NSFilePresenter

Для больших данных лучше подойдёт NSFileCoordinator, благодаря которому можно обеспечить согласованность чтения и записи. Это позволит избежать повреждения данных, так как существует вероятность, что к ним одновременно могут обращаться сразу несколько процессов.

URL-адрес общего контейнера получаем следующим образом:

let sharedUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.maxial.onemoreapp")

Запись:

fileCoordinator.coordinate(writingItemAt: sharedUrl, options: [], error: nil) catch { print(error) }
}

Чтение:

fileCoordinator.coordinate(readingItemAt: sharedUrl, options: [], error: nil) { newUrl in do { let data = try Data(contentsOf: newUrl) if let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: data) as String? { self.object = object } } catch { print(error) }
}

Стоит учесть, что NSFileCoordinator работает синхронно. Пока какой-либо файл будет занят некоторым процессом, другим придётся ждать его освобождения.

Это протокол, реализация которого может выглядеть следующим образом: Если нужно, чтобы App Extension знало, когда Containing App меняет состояние данных, используется NSFilePresenter.

extension TodayViewController: NSFilePresenter { var presentedItemURL: URL? { let sharedUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.maxial.onemoreapp") return sharedUrl?.appendingPathComponent("Items") } var presentedItemOperationQueue: OperationQueue { return .main } func presentedItemDidChange() { }
}

Свойство presentedItemOperationQueue возвращает очередь, которая используется для обратных вызовов при изменении файлов. Метод presentedItemDidChange() вызовется, когда какой-либо процесс, в этом случае Containing App, изменит содержимое данных. Если изменения были сделаны напрямую с помощью низкоуровневых вызовов записи, то presentedItemDidChange() не вызывается. Учитываются только изменения с использованием NSFileCoordinator.

При инициализации объекта NSFileCoordinator рекомендуется передавать объект NSFilePresenter, особенно если он запускает какую-либо файловую операцию:

let fileCoordinator = NSFileCoordinator(filePresenter: self)

Иначе объект NSFilePresenter будет получать уведомления об этих операциях, что может привести к взаимоблокировке при работе в одном потоке.

Чтобы начать отслеживание состояния данных, нужно вызвать метод addFilePresenter(_:) с соответствующим объектом:

NSFileCoordinator.addFilePresenter(self)

Любые созданные позже объекты NSFileCoordinator автоматически будут знать об этом объекте NSFilePresenter и уведомлять об изменениях, происходящих в его директории.

Чтобы перестать отслеживать состояние данных, используется removeFilePresenter(_:):

NSFileCoordinator.removeFilePresenter(self)

Core Data

Для совместного доступа к данным можно использовать SQLite и, соответственно, Core Data. Они умеют управлять процессами, работающими с общими данными. Чтобы настроить Core Data на совместный доступ для Containing App и App Extension, создадим подкласс NSPersistentContainer и переопределим метод defaultDirectoryURL, который должен возвращать адрес хранилища данных:

class SharedPersistentContainer: NSPersistentContainer { override open class func defaultDirectoryURL() -> URL { var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.maxial.onemoreapp") storeURL = storeURL?.appendingPathComponent("OneMoreApp.sqlite") return storeURL! }
}

В AppDelegate изменим свойство persistentContainer. Оно автоматически создаётся, если при создании проекта поставить галочку напротив Use Core Data. Теперь будем возвращать объект класса SharedPersistentContainer:

lazy var persistentContainer: NSPersistentContainer = { let container = SharedPersistentContainer(name: "OneMoreApp") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container
}()

Остается только добавить .xcdatamodeld в App Extension. Выбираем в панели навигатора файл .xcdatamodeld. В File Inspector в разделе Target Membership ставим галочку напротив App Extension:

Таким образом, Containing App и App Extension смогут читать и записывать данные в одно хранилище и использовать одну модель.

Запуск Containing App из App Extension

Когда Host App отправляет запрос App Extension, оно предоставляет extensionContext. Этот объект имеет метод open(_:completionHandler:), с помощью которого можно открыть Containing App. Однако не для всех типов App Extension доступен этот метод. В iOS он поддерживается Today Extension и iMessage Extension. iMessage Extension может использовать его только для открытия Containing App. Если Today Extension открывает с помощью него другое приложение, для отправки в App Store может потребоваться дополнительная проверка.

Чтобы открыть приложение из App Extension, нужно в Containing App определить URL Scheme:

Далее вызвать метод open(_:completionHandler:) с этой схемой из App Extension:

guard let url = URL(string: "OneMoreAppUrl://") else { return }
extensionContext?.open(url, completionHandler: nil)

Для тех типов App Extensions, которым вызов метода open(_:completionHandler:) недоступен, также существует способ. Но есть вероятность, что приложение может быть отклонено при проверке в App Store. Суть способа заключается в проходе по цепи объектов UIResponder до тех пор, пока не найдётся UIApplication, который и примет вызов openURL:

guard let url = URL(string: "OneMoreAppUrl://") else { return } let selectorOpenURL = sel_registerName("openURL:")
var responder: UIResponder? = self while responder != nil { if responder?.responds(to: selectorOpenURL) == true { responder?.perform(selectorOpenURL, with: url) } responder = responder?.next
}

App Extensions привнесли много нового в iOS-разработку. Постепенно появляется больше типов App Extensions, развиваются их возможности. Например, с выходом iOS 12 SDK теперь можно взаимодействовать с областью контента в уведомлениях, чего так давно не хватало.

Таким образом, Apple продолжает развивать этот инструмент, что внушает оптимизм по поводу его дальнейшего будущего.

Полезные ссылки:

Официальная документация
Sharing data between iOS apps and app extensions
iOS 8 App Extension Development Tips

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть