Уведомления в 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 теперь можно показывать полученные уведомления в активном приложении. Они, как обычно, будут всплывать прямо над статус баром и иметь системный дизайн и поведение.

35a82caeba6148dcba01dcbf96d8f6e1.gif

Эта возможность по умолчанию выключена, ее нужно активировать в методе делегата UNUserNotificationCenterDelegate. Если приложение активно, то перед тем как отобразить уведомление у вас будет возможность его обработать:

@available(iOS 10, *)
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert, .sound, .badge])
}

Чтобы не выводить уведомление достаточно вызвать обработчик с пустыми параметрами completionHandler([]) или удалить реализацию этого метода совсем.


Управление уведомлениями

Уведомление теперь можно удалить или обновить его содержимое, пока оно ещё не прочитано. Система использует идентификатор для обновления полученного или ожидающего доставки уведомления. Если будет получено новое уведомление с существующим идентификатором, то вместо того, чтобы показывать несколько уведомлений подряд, старое уведомление обновится и поднимется выше в списке уведомлений.

c9e8ae97c356456ab7747399344691bc.gif

Идентификатор — это параметр, который передается во время создания запроса для локального уведомления UNNotificationRequest(identifier: "identifier", content: content, trigger: trigger). В пуш-уведомлениях он передается с помощью HTTP/2 заголовка apns-collapse-id. Неизвестно, можно ли заставить работать этот заголовок на старых протоколах. Как минимум, это ещё одна причина перейти наконец на HTTP/2 протокол для тех, кто этого ещё не сделал.


Вложения

Теперь можно вставлять картинки и видео в уведомления. Для этого существует новое расширение Notification Service Extension.


Notification Service Extension

86c4576a94954e2d960b0da7971f8c0a.png

В этом расширении есть всего два метода. Первый метод 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

b0fcaadc5172475588aaaee59e6f444b.png

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

aa00d8b79c5a4a1f93943c3605004b95.gif


Изменение внешнего вида (UI)

Теперь появилась возможность добавить в уведомление любой контент. Это делается с помощью расширения Notification Content Extension.


Notification Content Extension

a03723df2a39496fbedc652dd6dcd519.png

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

2c337ecd4d074a9cbffcc5dd9ee14e96.gif

Расширение состоит из контроллера UIViewController и сториборда.

ebd0eb1358e44601a6ae26fa10b42529.png

Система запустит расширение, когда получит уведомление с категорией, указанной в Info.plist. Можно перечислить несколько категорий:

7134e1b640524d97a9a0587659c3ce2c.png

{
    "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

© Habrahabr.ru