Уведомления в 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

Поделиться с друзьями
-->

Комментарии (10)


  1. fiveze
    24.06.2016 10:29

    Спасибо за гайд. Есть что поделать с новыми пушами теперь.


  1. artyfarty
    24.06.2016 11:42

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


  1. olegi
    26.06.2016 20:22

    показывать стандартный UI в активном приложении (why so long!11)
    а в чем use case такой штуки?


    1. muxx
      26.06.2016 21:31
      +1

      Например, в мессенджере общаешься с одним пользователем, открыт диалог с ним, и в этот момент приходит ответ от другого.


  1. N_Shelest
    26.06.2016 22:44

    А на Android или его кастомных прошивках есть что-то подобное?
    Если нет, думаю, велика вероятность встретить в следующей андро-сладости.


    1. ad1Dima
      27.06.2016 13:43
      -1

      На android изначально была возможность контроля нотификаций: показывать ли их вообще, удалить и т.д.
      Кастомного UI вроде нет, но картинки отображать можно.
      На Windows возможность удалять уведомление есть давно, по кастомизации только картинки можно показывать.

      Ну а действия и ответ на сообщения есть на всех трех платформах.


  1. alekssamos
    26.06.2016 22:44

    удалять себя из центра уведомлений
    То чувство, когда тебе в каком-нибудь мессенджере или в скайпе написали сообщение, затем удалили или отредактировали его и осталась у тебя только одна надежда, посмотреть сообщение в уведомлении на экране блокировки… :-)


    1. ad1Dima
      27.06.2016 13:43

      А мне вот нравится, что слек удаляет нотификации прочитанных сообщений с телефона.


  1. vani2
    30.06.2016 12:20

    Получается, при открытии приложения с уведомления (тапом или свайпом с заблокированного экрана) на iOS 10 обработка остается такой же (в методе didFinishLaunching)?


    1. zaazza
      30.06.2016 12:33

      да, методы апделегата остаются в силе