iOS Нотификации. Подписка и рассылка
Все просто, но не очень. В интернете куча статей про нотификации в иос. И в этом проблема - слишком много статей, часть из них уже не актуальны и большинство очень поверхностны. Поэтому, я решил добавить еще одну статью и хорошенько во всем разобраться.
Нотификации в приложении генерируются из-за событий в самом приложении (например, по таймеру) или по сообщению с сервера. Первые называются локальными, а вторые – пуш-нотификациями.
Пуш-нотификации работают через APNs (Apple Push Notification service). Для отправки сообщения пользователю нужно сформировать запрос к серверу APNs. Это делается разными способами.
- Через token соединение.
- Через соединение с помощью сертификата.
Отправка соединений с помощью токена выглядит попроще - ей и займемся.
Локальные нотификации
Вся логика будет реализована в классе Notifications
. Перед началом работы с нотификациями импортируем UserNotifications
1import UserNotifications
Запрашиваем разрешение у пользователя на отправку нотификаций. Для этого в классе Notifications
добавляем метод
1let center = UNUserNotificationCenter.current()
2
3func requestAuthorisation() {
4 center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
5 print("Permission granted: \(granted)")
6 }
7}
В классе AppDelegate
добавим новое свойство notifications
и вызовем метод requestAuthorisation
при старте приложения
1let notifications = Notifications()
2
3func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
4 notifications.requestAuthorisation()
5
6 return true
7}
Пользователь может поменять настройки уведомлений. Нужно не только запрашивать авторизацию, но и проверять настройки сообщений при старте приложения. Реализуем метод getNotificationSettings()
и изменим requestAuthorisation()
- и добавим получение настроек нотификаций, если requestAuthorization
возвращает granted == true
1func requestAuthorisation() {
2 center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
3 print("Permission granted: \(granted)")
4
5 guard granted else {
6 return
7 }
8
9 self.getNotificationSettings()
10 }
11}
12
13func getNotificationSettings() {
14 center.getNotificationSettings { settings in
15 print("Notification settings : \(settings)")
16 }
17}
Создадим локальное уведомление. Для этого добавим метод scheduleNotification()
в классе AppDelegate`. В нем будем задавать нотификации по расписанию.
1func scheduleNotification(type: String) {
2 let content = UNMutableNotificationContent()
3
4 content.title = type
5 content.body = "Example notification " + type
6 content.sound = .default
7 content.badge = 1 // красный бейджик на иконке с кол-вом непрочитанных сообщений
8}
Для создания уведомления используем класс UNMutableNotificationContent
. Подробней о возможностях этого класса в документации.
Триггер для показа уведомления может срабатывать по времени, календарю или местоположению. Можно отправлять уведомления каждый день в определенное время или раз в неделю.
Мы будем слать уведомления по времени. Создадим соответствующий триггер.
1content.sound = .default
2content.badge = 1 // красный бейджик на иконке с кол-вом непрочитанных сообщений
3
4let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
5let id = "Local Notification #1"
6
7let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
8
9notifications.add(request) { error in
10 if let error = error {
11 print("Error \(error.localizedDescription)")
12 }
13}
Сначала создаем trigger
- триггер, который будет срабатывать через 5 секунд. Задаем идентификатор для нашего уведомления id
. Он должен быть уникальным для каждого уведомления.
Теперь у нас есть все, чтобы создать запрос на показ уведомления и добавить его в центр уведомлений UNUserNotificationCenter
. Для этого делаем вызов notifications.add(request)
Осталось вызвать метод scheduleNotification(type: String)
. В любой контроллер добавим делегат:
1let delegate = UIApplication.shared.delegate as? AppDelegate
Добавим кнопку и по нажатию вызовем нужный метод
1delegate?.scheduleNotification(type: "local")
Если нажать кнопку, то через 5 секунд появится уведомление как на картинке. Не забывайте, что нужно свернуть приложение, чтобы увидеть уведомление.
На иконке появился бейджик. Сейчас он остается на всегда и не пропадает. Давайте это поправим - добавим несколько строчек кода в AppDelegate
1func applicationDidBecomeActive(_ application: UIApplication) {
2 UIApplication.shared.applicationIconBadgeNumber = 0
3}
При каждом запуске приложения количество непрочитанных уведомлений будет обнуляться.
Уведомления когда приложение не в бекграунде
Есть возможность получать уведомления, даже когда приложение на переднем плане. Реализуем протокол UNUserNotificationCenterDelegate
. Для этого добавим новый экстеншен.
В документации по протоколу UNUserNotificationCenterDelegate
сказано
Use the methods of the UNUserNotificationCenterDelegate protocol to handle user-selected actions from notifications, and to process notifications that arrive when your app is running in the foreground.
Нам нужно использовать метод func userNotificationCenter(UNUserNotificationCenter, willPresent: UNNotification, withCompletionHandler: (UNNotificationPresentationOptions) -> Void)
про который написано
Asks the delegate how to handle a notification that arrived while the app was running in the foreground.
Это как раз то, чего мы хотим добиться. Подпишем класс Notifications
под протокол UNUserNotificationCenterDelegate
.
1extension Notifications: UNUserNotificationCenterDelegate {
2 public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> ()) {
3 completionHandler([.alert, .sound])
4 }
5}
И укажем делегат перед вызовом метода requestAuthorisation()
в классе AppDelegate
.
1func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
2 notifications.requestAuthorisation()
3 notifications.center.delegate = notifications // не самый лучший код, но для примера сгодится
4 return true
5}
Обработка уведомлений
При тапе на уведомление открывается приложение. Это поведение по умолчанию. Чтобы мы могли как-то реагировать на нажатия по уведомлениям - нужно реализовать еще один метод протокола UNUserNotificationCenterDelegate
.
1public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> ()) {
2 if response.notification.request.identifier == "Local Notification #1" {
3 print("Received notification Local Notification #1")
4 }
5
6 completionHandler()
7}
Действия для уведомлений
Чтобы добавить кастомные действий в уведомлениях, сначала нужно нужно добавить категории уведомлений.
Добавляем кастомные экшены в методе scheduleNotification()
.
1let snoozeAction = UNNotificationAction(identifier: "snooze", title: "Snooze")
2let deleteAction = UNNotificationAction(identifier: "delete", title: "Delete", options: [.destructive])
Теперь создаем категорию с уникальным идентификатором.
1let userAction = "User Action"
2
3let category = UNNotificationCategory(
4 identifier: userAction,
5 actions: [snoozeAction, deleteAction],
6 intentIdentifiers: [])
7
8notifications.setNotificationCategories([category])
Метод setNotificationCategories()
регистрирует нашу новую категорию в центре уведомлений.
Осталось указать категорию при создании нашего уведомления. В месте, где мы создаем экземпляр класса UNMutableNotificationContent
, нужно установить параметр categoryIdentifier
.
1content.sound = .default
2content.badge = 1 // красный бейджик на иконке с кол-вом непрочитанных сообщений
3content.categoryIdentifier = userAction
У нас появились кастомные действия. Их будет видно, если потянуть уведомление вниз. Но они пока ничего не делают.
Добавим обработку стандартных и кастомных действий в экстеншене.
1public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> ()) {
2 if response.notification.request.identifier == "Local Notification #1" {
3 print("Received notification Local Notification #1")
4 }
5
6 print(response.actionIdentifier)
7
8 switch response.actionIdentifier {
9 case UNNotificationDismissActionIdentifier:
10 print("Dismiss action")
11 case UNNotificationDefaultActionIdentifier:
12 print("Default action")
13 case "snooze":
14 print("snooze")
15 scheduleNotification(type: "Reminder")
16 case "delete":
17 print("delete")
18 default:
19 print("undefined")
20 }
21
22 completionHandler()
23}
UNNotificationDefaultActionIdentifier
- срабатывает при нажатии по уведомлению. UNNotificationDismissActionIdentifier
- срабатывает, когда мы смахиваем уведомление вниз. С Dismiss есть один неочевидный момент - он не будет работать, если при создании категории не указать опцию .customDismissAction
:
1let category = UNNotificationCategory(identifier: userAction,
2 actions: [snoozeAction, deleteAction],
3 intentIdentifiers: [],
4 options: .customDismissAction)
На сайте документации есть две статьи по теме кастомных действий:
- Handling Notifications and Notification-Related Actions
- Declaring Your Actionable Notification Types
Пользовательский контент
Для уведомлений можно устанавливать кастомные изображения. Добавим его в методе scheduleNotification(type: String)
1guard let icon = Bundle.main.url(forResource: "icon", withExtension: "png") else {
2 print("Path error")
3 return
4}
5
6do {
7 let attach = try UNNotificationAttachment(identifier: "icon", url: icon)
8 content.attachments = [attach]
9} catch {
10 print("Attachment error")
11}
Картинка должна быть в файлах проекта, не в папке Assets.xcassets. Иначе, метод Bundle.main.url
вернет nil
. Если все сделано правильно – уведомление будет выглядеть как-то так:
На этом с локальными уведомлениями все.
Пуш-уведомления
Для работы с такими уведомлениями вам нужен платный аккаунт разработчика.
Пуш-уведомления отправляются с сервера через APNs. Уведомления приходят на разные девайсы, APNs сам маршрутизирует сообщения. Разработчик сам решает, когда отправить уведомление.
Для отправки пуш-уведомлений необходимо выполнить дополнительные манипуляции. Схема ниже показывает нужные шаги.
- Приложение регистрируется для отправки сообщений.
- Девайс получает специальный токен с APNs сервера.
- Токен передается в приложение.
- Приложение отправляет токен провайдеру(например, нашему бэкенду)
- Теперь провайдер может слать уведомления через APNs с использованием токена, который сохранили на 4 шаге.
Существует 2 вида пуш-уведомлений: тестовые(sandbox) и реальные(production). Для разных видов уведомлений используются разные APNs сервера.
Чтобы приложение могло зарегистрироваться для оправки соединения - нужно включить поддержку поддержку пуш-уведомлений. Проще всего это сделать с помощью Xcode. Раньше это был довольно замороченный процесс, но сейчас достаточно выбрать Push Notifications.
И сразу добавьте поддержку бэкграунд обработку задач. Должно быть как на картинке.
За кадром сгенерируется новый идентификатор приложения, обновится Provisioning Profile. Идентификатор моего приложения ru.4gophers.Notifications. Его можно найти на страничке https://developer.apple.com/account/resources/identifiers/list
В настройках этого идентификатора уже должна быть указана поддержка пуш-уведомлений.
И в проекте появляется новый файл Notifications.entitlements. Этот файл имеет расширение .entitlements и называется как и проект.
Сертификаты
Теперь нам нужно создать CertificateSigningRequest для генерации SSL сертификата пуш-уведомлений. Это делается с помощью программы Keychain Access
Сгенерированный файл CertificateSigningRequest.certSigningRequest сохраните на диск. Теперь с его помощью генерируем SSL сертификаты для отправки пуш-уведомлений. Для этого на страничке https://developer.apple.com/account/resources/identifiers/list выберите ваш идентификатор, в разделе Push Notifications нажмите кнопку Сonfigure и сгенерируйте новый Development SSL сертификат с помощью файла CertificateSigningRequest.
Скачайте сгенерированный сертификат и установите его в системе(просто кликните по нему). В программе Keychain Access должен показаться этот серт:
Отлично! Теперь экспортируем сертификат с помощью все той же программы Keychain Access. Нажимаем правой кнопкой по сертификату и выбираем экспорт:
При экспорте нужно выбрать расширение файла .p12. Этот экспортированный сертификат понадобится нам в будущем.
Пуш-уведомления можно тестировать только на реальных устройствах. Девайс должен быть зарегистрирован в https://developer.apple.com/account/resources/devices/list и у вас должен быть рабочий сертификат разработчика.
Осталось добавить ключ для пуш-уведомлений. Для этого на страничке https://developer.apple.com/account/resources/authkeys/list нажимаем + добавляем новый ключ:
Я назову ключ Push Notification Key. После создания ключа, обязательно скачайте его, нажав на кнопку Done
Получение пуш-уведомлений
С подготовкой закончили, вернемся к коду. В методе getNotificationSettings()
регистрируем наше приложение в APNs для получения пуш-уведомлений.
1func getNotificationSettings() {
2 center.getNotificationSettings { settings in
3 print("Notification settings : \(settings)")
4
5 guard settings.authorizationStatus == .authorized else {
6 return
7 }
8
9 // регистрироваться необходимо в основном потоке
10 DispatchQueue.main.async {
11 UIApplication.shared.registerForRemoteNotifications()
12 }
13 }
14}
Теперь в классе AppDelegate
нужно добавить пару методов. Получаем девайс токен:
1func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
2 let parts = deviceToken.map { data in
3 return String(format: "%02.2hhx", data)
4 }
5
6 let token = parts.joined()
7 print("Device token: \(token)")
8}
Этот токен нам нужен для отправки уведомлений. Он работает как адрес приложения. В реальном приложении мы отправим его наш бекенд и сохраним в базе.
Обработаем ситуацию когда что-то пошло не так и нам не получилось зарегистрироваться в APNs.
1func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
2 print("Failed registration: \(error.localizedDescription)")
3}
Забавно, но у меня ничего не заработало сразу. Ни метод didRegisterForRemoteNotificationsWithDeviceToken
, ни didFailToRegisterForRemoteNotificationsWithError
не срабатывали. Я потратил на поиск проблемы несколько часов, пока случайно не наткнулся на это обсуждение. Выключите и включите вай-фай. Да. Не спрашивайте.
Отправка нотификаций
Все готово для отправки и получения уведомлений. Давайте протестируем.
Десктопное приложение
Приложений для тестирования уведомлений целая куча, но мне больше всего нравится PushNotifications. Переключитесь на вкладку TOKEN и укажите нужные данные.
Сначала попробуем отправить сообщение с помощью ключа Push Notification Key.
- f6c10036b6203ebf40a246ce5a741c3b17778063c78aa1016c6474d3dfef46e2 – Токен, который мы получаем при запуске приложения. Он выводится в консоль.
- YYS33CP3HU – Идентификатор ключа, который мы сгенерировали выше и назвали Push Notification Key
- 25K6PDW2HY – Team ID, идентификатор аккаунта разработчика
Тело самого уведомления - обычный JSON
1{
2 "aps": {
3 "alert": "Hello2" // это тело уведомления
4 },
5 "yourCustomKey": "1" // любые кастомные данные
6}
alert
может быть объектом с заголовком и телом. В уведомление можно указывать звук, бейдж. thread-id
позволяет группировать уведомления. Ключ category
позволяет использовать кастомные экшены. content-available
обозначает досупность обновления для уведомления в бэкграунд режиме.
1{
2 "aps": {
3 "alert": {
4 "title": "Hello",
5 "body": "Тут можно много всего написать"
6 },
7 "sound": "default",
8 "badge": 10,
9 "thread-id": 1,
10 "category": "User Action",
11 "content-available": 1
12 },
13 "yourCustomKey": "1"
14}
Для отправки нотификаций можно использовать не только .p8 ключ, но и наш SSL сертификат, который мы сгенерировали ранее. Для этого в приложении PushNotifications есть вкладка CERTIFICATE. Она работает точно так же, только нужно использовать сертификат .p12, указать пароль и не нужно указывать Team ID.
Обработка кастомных параметров
Для получения данных из пуш-уведомления нужно реализовать метод в AppDelegate
.
1func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
2 print(userInfo)
3}
Но этот метод позволяет получить данные уже после показа уведомления. А в iOS есть возможность кастомизировать контент уведомления с помощью экстеншенов. Например, можно задавать кастомную картинку для каждого уведомления. Для этого нужно создать расширение Notification Content Extension как показано на скриншотах.
Также, можно менять данные в нотификациях перед их показом с помощью Notification Service Extension. Но тема создания таких расширений слишком обширна и тянет на отдельную статью.
Используем Go библиотеку
И теперь самое простое - отправка уведомлений с использованием Go. Уже есть множество готовых библиотек, нам нужно выбрать самую удобную и научится с ней работать.
Мне больше всего понравился пакет APNS/2. В этом пакете уже есть готовая консольная утилита для отправки уведомлений. И у него очень простое АПИ.
Создаем клиент, который будет отправлять сообщения с помощью .p8 ключа.
1package main
2
3import (
4 "fmt"
5 "log"
6
7 "github.com/sideshow/apns2"
8 "github.com/sideshow/apns2/token"
9)
10
11func main() {
12
13 authKey, err := token.AuthKeyFromFile("./AuthKey_YYS33CP3HU.p8")
14 if err != nil {
15 log.Fatal("token error:", err)
16 }
17
18 token := &token.Token{
19 AuthKey: authKey,
20 KeyID: "YYS33CP3HU",
21 TeamID: "25K6PDW2HY",
22 }
23
24 notification := &apns2.Notification{}
25 notification.DeviceToken = "f6c10036b6203ebf40a246ce5a741c3b17778063c78aa1016c6474d3dfef46e2"
26 notification.Topic = "ru.4gophers.Notifications"
27 notification.Payload = []byte(`{"aps":{"alert":"Hello!"}}`)
28
29 client := apns2.NewTokenClient(token)
30 res, err := client.Push(notification)
31
32 if err != nil {
33 log.Fatal("Error:", err)
34 }
35
36 fmt.Printf("%v %v %v\n", res.StatusCode, res.ApnsID, res.Reason)
37}
Такой простой код позволяет отправлять сообщения из Go-приложения на iOS телефон. В приложении может быть хендлер, который будет сохранять DeviceToken в базу. И вы сможете рассылать любые уведомления в любое время.
Ссылки
- Исходнки к статье на GitHub.
- Официальная документация по нотификациям.
- Приложение для тестирования нотификаций.
- Курс на swiftbook.ru.
- Push Notifications Tutorial: Getting Started.
- Кастомное отображение нотификаций с помощью расширений.
- APNS/2 - либа для отправки пуш-уведомлений.