Уведомления в iOS 10
Говорят, что на этом WWDC не было ничего интересного, кроме интерактивных уведомлений. Действительно, новые уведомления одна из самых интересных новых фич. Не только для разработчиков, но и для простых пользователей. В iOS 10 попытались унифицировать работу с локальными и пуш-уведомлениями и добавили для этого новый фреймворк UserNotifications.framework
. Старое API теперь запрещено (deprecated), но его можно использовать до тех пор, пока вы поддерживаете iOS 9.
Новые уведомления умеют:
- показывать вложения (картинки и видео)
- отображать кастомный UI
- показывать стандартный UI в активном приложении (why so long!11)
- удалять себя из центра уведомлений (!1)
В этой статье разберемся как это работает. Будет интересно не только разработчикам, но и UX проектировщикам.
Регистрация уведомлений
Управлением уведомлениями теперь занимается класс UNUserNotificationCenter
. Как и раньше, во время регистрации надо указать типы уведомлений, которые система будет обрабатывать (.alert
, .sound
, .badge
). Но, вместо типа UIUserNotificationType
, эти значения теперь имеют тип UNAuthorizationOptions
. Подписка на уведомления теперь происходит так:
UNUserNotificationCenter.current().requestAuthorization([.alert, .sound, .badge]) { (granted, error) in
if granted {
UIApplication.shared().registerForRemoteNotifications()
}
}
Старые методы работы с уведомлениями будут ещё актуальны несколько лет, поэтому нужно позаботиться об их поддержке. Для совместимости c iOS8+, сделаем метод, который позволит конвертировать список опций из UIUserNotificationType
в UNAuthorizationOptions
:
extension UIUserNotificationType {
@available(iOS 10.0, *)
func authorizationOptions() -> UNAuthorizationOptions {
var options: UNAuthorizationOptions = []
if contains(.alert) {
options.formUnion(.alert)
}
if contains(.sound) {
options.formUnion(.sound)
}
if contains(.badge) {
options.formUnion(.badge)
}
return options
}
}
Теперь легко подписаться на пуши так, чтобы поддерживать оба API:
func registerForNotifications(types: UIUserNotificationType) {
if #available(iOS 10.0, *) {
let options = types.authorizationOptions()
UNUserNotificationCenter.current().requestAuthorization(options) { (granted, error) in
if granted {
self.application.registerForRemoteNotifications()
}
}
} else {
let settings = UIUserNotificationSettings(types: types, categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
}
}
Отправка уведомлений
Для отправки уведомлений существуют две новых сущности: триггер UNNotificationTrigger
и запрос UNNotificationRequest
.
Триггер UNNotificationTrigger
нужен для того, чтобы задать условие, при котором будет доставлено уведомление. Существует четыре вида триггеров:
UNPushNotificationTrigger
— устанавливается системой автоматически при получении пуша.UNTimeIntervalNotificationTrigger
— сработает через заданный промежуток времениUNCalendarNotificationTrigger
— сработает в определенное время в будущем.UNLocationNotificationTrigger
— сработает при входе/выходе из заданного георегиона.
Запрос UNNotificationRequest
используется, чтобы отправить уведомление системе. Также, система передаст вам этот объект, когда получит уведомление. Его удобно использовать, чтобы проверить тип уведомления и перейти в приложении на определенный экран.
Так можно отправить локальное уведомление, сохранив совместимость со старыми версиями iOS:
func scheduleNotification(identifier: String, title: String, subtitle: String, body: String, timeInterval: TimeInterval, repeats: Bool = false) {
if #available(iOS 10, *) {
let content = UNMutableNotificationContent()
content.title = title
content.subtitle = subtitle
content.body = body
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: repeats)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
} else {
let notification = UILocalNotification()
notification.alertBody = "\(title)\n\(subtitle)\n\(body)"
notification.fireDate = Date(timeIntervalSinceNow: 1)
application.scheduleLocalNotification(notification)
}
}
Отображение в активном приложении
В iOS 10 теперь можно показывать полученные уведомления в активном приложении. Они, как обычно, будут всплывать прямо над статус баром и иметь системный дизайн и поведение.
Эта возможность по умолчанию выключена, ее нужно активировать в методе делегата UNUserNotificationCenterDelegate
. Если приложение активно, то перед тем как отобразить уведомление у вас будет возможность его обработать:
@available(iOS 10, *)
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound, .badge])
}
Чтобы не выводить уведомление достаточно вызвать обработчик с пустыми параметрами completionHandler([])
или удалить реализацию этого метода совсем.
Управление уведомлениями
Уведомление теперь можно удалить или обновить его содержимое, пока оно ещё не прочитано. Система использует идентификатор для обновления полученного или ожидающего доставки уведомления. Если будет получено новое уведомление с существующим идентификатором, то вместо того, чтобы показывать несколько уведомлений подряд, старое уведомление обновится и поднимется выше в списке уведомлений.
Идентификатор — это параметр, который передается во время создания запроса для локального уведомления UNNotificationRequest(identifier: "identifier", content: content, trigger: trigger)
. В пуш-уведомлениях он передается с помощью HTTP/2 заголовка apns-collapse-id
. Неизвестно, можно ли заставить работать этот заголовок на старых протоколах. Как минимум, это ещё одна причина перейти наконец на HTTP/2 протокол для тех, кто этого ещё не сделал.
Вложения
Теперь можно вставлять картинки и видео в уведомления. Для этого существует новое расширение Notification Service Extension.
Notification Service Extension
В этом расширении есть всего два метода. Первый метод didReceiveRequest:withCompletionHandler
используется для вставки медиа файлов. Второй метод serviceExtensionWithExpire
вызовется, если расширение не успеет выполниться в отведенный промежуток времени:
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler:(UNNotificationContent) -> Void) {
// handle attachments
}
override func serviceExtensionTimeWillExpire() {
// fallback to default message
}
}
Расширению выделяется 30 секунд, чтобы успеть скачать вложение и сохранить его во временный файл. Поэтому в пушах необходимо передавать ссылки на оптимизированные ресурсы. Изображения должны быть маленькими, а видео короткими. Если, расширение не успело вызвать contentHandler
в отведенное время, то оно будет уничтожено и полученное уведомление отобразится без изменений.
Чтобы расширение выполнилось в пуш-уведомлении должен присутствовать ключ mutable-content: 1
. Типичный пример пуш-уведомления с вложением:
{
"aps": {
"alert": "Привет как дела?",
"mutable-content": 1
},
"image": "https://habrastorage.org/files/ff5/03e/e6b/ff503ee6b45d46ffb092aac33f2f282b.gif"
}
Прежде чем вложение появится на экране, его нужно скачать и сохранить во временный файл. Файлы должны иметь тип, который поддерживается системой:
https://developer.apple.com/reference/usernotifications/unnotificationattachment#overview
Скачать файл можно так (тут и далее, форсированый try!
используется для краткости):
func store(url: URL, extension: String, completion: ((URL?, NSError?) -> ())?) {
// obtain path to temporary file
let filename = ProcessInfo.processInfo().globallyUniqueString
let path = try! URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(filename).\(`extension`)")
// fetch attachment
let task = session.dataTask(with: url) { (data, response, error) in
let _ = try! data?.write(to: path)
completion?(path, error)
}
task.resume()
}
Затем уведомление нужно модифицировать, добавив в него объект UNNotificationAttachment
. В конце всегда нужно вызывать contentHandler
:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler:(UNNotificationContent) -> Void) {
let content = request.content.mutableCopy() as! UNMutableNotificationContent
if let gif = request.content.userInfo["gif"] as? String {
let url = URL(string: gif)!
attachmentStorage.store(url: url, extension: "gif") { (path, error) in
if let path = path {
let attachment = try! UNNotificationAttachment(identifier: "image", url: path, options: nil) {
content.attachments = [attachment]
contentHandler(content)
} else {
contentHandler(content)
}
}
} else {
contentHandler(request.content)
}
}
При сильном нажатии оно разворачивается в детальное представление и вложение при этом показывается полностью. Если во вложении гифка, то она начинает проигрываться. Пока что поддерживается только 3D Touch, но Apple обещает портировать эту возможность на устройства с обычными тачами.
Изменение внешнего вида (UI)
Теперь появилась возможность добавить в уведомление любой контент. Это делается с помощью расширения Notification Content Extension.
Notification Content Extension
Это расширение используется для того, чтобы изменить внешний вид уведомления, когда оно представлено в развернутом виде. К сожалению, такой контент не интерактивен и нажатие на него невозможно. Все взаимодействие строится на основе кнопок действий под уведомлением.
Расширение состоит из контроллера UIViewController
и сториборда.
Система запустит расширение, когда получит уведомление с категорией, указанной в Info.plist. Можно перечислить несколько категорий:
{
"aps": {
"alert": "Списано 32₽",
"category": "content"
}
}
Конечно же, категория должна быть зарегистрирована заранее:
let category = UNNotificationCategory(identifier: "content", actions: [], minimalActions: [], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
Кроме того в Info.plist можно задать следующие флаги:
UNNotificationExtensionDefaultContentHidden
— управляет отображением стандартных надписей с заголовком и текстом уведомленияUNNotificationExtensionInitialContentSizeRatio
— соотношение ширины уведомления к высоте. Система использует это значение для задания стартового размера уведомления на экране.
Knuff App
Для отправки пуш уведомлений в этой статье использовалось приложение Knuff App. Очень удобно. Информация о ней была в одном из наших дайджестов MBLTDev 58.
Спасибо
Если вы дочитали до этого места, значит вам правда было интересно. Спасибо за внимание. Надеюсь, было полезно.
Ссылки
Introduction to Notifications
Advanced Notifications
iOS Human Interface Guidelines
UserNotifications Framework Reference
UserNotificationsUI Framework Reference
Knuff App