Por parte do usuário, o cliente de email é um aplicativo simples. Os desenvolvedores do Yandex.Mail até brincam que existem apenas três telas no aplicativo: uma lista de letras; enviando uma carta; sobre a tela.
Mas muitas coisas interessantes estão acontecendo sob o capô. Como muitos aplicativos móveis, o Mail usa notificações push para interagir com os usuários. Como muitos aplicativos iOS, o Mail perde algumas notificações devido à natureza do Apple Push Notification Service.
Asya Sviridenko , chefe do grupo iOS do Yandex.Mail, provará que, mesmo com as limitações do sistema, a perda de notificações por push pode e deve ser combatida se forem críticas para o seu aplicativo. Isso vale para o Mail, porque as notificações por push de novas letras são o que o usuário instala o aplicativo. Se, para o seu aplicativo, a entrega de notificações por push não é tão crítica, ainda é interessante descobrir quais bicicletas o Yandex.Mail móvel empilhou.
Trata-se de notificação remota, ou seja, notificações provenientes do servidor por meio de APNs (Apple Push Notification Service). Não tocaremos nas notificações locais e falaremos sobre:
- Como é a API para trabalhar com notificações por push. Considere um esquema de entrega de notificação por push e onde as perdas podem ocorrer nesse esquema.
- Como você decidiu lidar com as perdas no Yandex.Mail - a fila de notificações por push.
- Como registrar e que outras dificuldades podem encontrar.
O que temos e onde perdemos
Agora, a API para trabalhar com notificações push é uma coisa bastante poderosa que permite fazer muitas coisas interessantes. Mas esse nem sempre foi o caso.

Anteriormente, as notificações por push eram exatamente assim - era um infeliz painel azul que aparecia na tela, bloqueava o trabalho com o aplicativo atual, não permitia que nada fosse feito e depois desaparecia para sempre, e não havia mais lembretes sobre isso.
Tempo suficiente se passou desde então.

Para nós, como desenvolvedores,
tudo começou no iOS 3 quando as notificações por push foram disponibilizadas para bibliotecas de terceiros.
O Notification Center apareceu no iOS 5 e as notificações por push deixaram de ir a lugar nenhum; agora permanecem no Notification Center, onde podem ser visualizadas novamente.
O IOS 6 introduziu Não perturbe . O usuário tem a oportunidade de definir o período durante o qual ele não deseja receber notificações.
Essas mudanças diziam respeito principalmente a como o usuário pode trabalhar com notificações por push, como ele pode tornar sua vida mais confortável e não como os desenvolvedores podem influenciar as notificações.
Para os desenvolvedores, um marco importante foi o
iOS 8 e o surgimento da Ação de Notificação , que permitiu executar ações específicas para um aplicativo específico por meio de notificações push.
O IOS 10 apresenta a extensão de serviço de notificação e a extensão de conteúdo de notificação . O primeiro permite modificar a notificação por push antes que ela seja mostrada ao usuário. O segundo é mostrar alguma interface do usuário por notificação por push na notificação por push, na qual, por exemplo, você pode exibir informações mais detalhadas. No iOS 10, essa interface não era clicável - você pode assistir, não pode tocá-la.
O IOS 11 introduziu as configurações de privacidade da notificação . Agora, o usuário pode acessar as configurações e indicar se deseja que o conteúdo das notificações recebidas seja exibido. Este é um grande passo em direção à segurança. Foram necessárias apenas 8 versões do iOS para entender que nem todos os usuários desejam que informações pessoais apareçam repentinamente no iPhone deitado sobre a mesa.
No iOS 12, tornou-se possível agrupar notificações push por ID de segmento, e a interface do usuário que recebemos no iOS 10 usando a extensão de conteúdo de notificação tornou-se clicável. Agora você pode adicionar botões e controles por gesto - tudo isso ajuda o usuário a interagir com a interface do usuário.
Notificações push hoje
Como você pode ver, as notificações por push percorreram um longo caminho e, hoje, com a ajuda delas, você pode realmente fazer muitas coisas.
Mensagens de texto e localização
Como antes, podemos enviar mensagens de texto em uma notificação por push, mas agora você pode especificar adicionalmente chaves para localização.
"aps" : { "alert" : { "title" : "New Mail", "subtitle-loc-key" : "alert_subtitle_localization_key", "loc-key" : "alert_body_localization_key", } }
Se você especificar
subtitle-loc-key
e
loc-key
na notificação de carga útil, quando a notificação por push chegar ao dispositivo, os valores necessários serão encontrados no arquivo Localizable.string do aplicativo e o usuário verá uma mensagem localizada.
Som e alerta crítico
Como antes, você pode adicionar sons às notificações de carga útil.
"aps" : { "sound" : { "critical" : 1, "name" : "bingbong.aiff", "volume" : 1.0, } }
O IOS 12 tem um alerta crítico. São sons que serão reproduzidos, mesmo que o usuário esteja no modo Não perturbe.
Normalmente, o usuário não precisa, por exemplo, de um aplicativo com uma assinatura de revista à noite para informar que um novo número foi lançado. Portanto, a Apple restringe aplicativos que podem usar alerta crítico. Se o seu aplicativo trabalha com saúde, segurança ou você acha que alerta crítico é algo que pode realmente ajudar os usuários a interagir com ele, escreva para a Apple. Talvez eles permitam que você use essa funcionalidade.
Notificações silenciosas
O usuário não vê notificações silenciosas. Eles chegam diretamente ao aplicativo, ativam e permitem executar algumas ações para atualizar o aplicativo: envie uma solicitação ao servidor, solicite dados em segundo plano, atualize dados do banco de dados, atualize a interface do usuário para que, quando o usuário entrar no aplicativo, ele veja dados atualizados.
"aps" : { "content-available" : 1
Para que a notificação por push fique silenciosa, você deve especificar na carga útil:
"content-available" : 1
. E não especifique chaves de alerta, som e emblema na carga útil - elas são completamente inúteis para notificações por push que não serão mostradas ao usuário.
Agrupamento de notificações
Para agrupar mensagens, você deve especificar "ID da thread" na carga útil. Pode ter vários valores no mesmo aplicativo, se você deseja agrupar de maneiras diferentes: por contas, por destinatários, por tópico.
"aps" : { "thread-id" : "any_thread_identifier" }
Isso é muito conveniente, porque agora as notificações por push não ocupam todo o espaço na tela bloqueada, mas são agrupadas. Se você ainda não estiver usando essa funcionalidade, é hora de começar.
Altere a notificação antes de mostrá-la
As notificações por push podem ser alteradas antes de serem exibidas. Para fazer isso, você precisa adicionar a extensão de conteúdo de notificação ao aplicativo e substituir o método
didReceive
. Nesse método, você pode obter o conteúdo da notificação e modificá-lo.
"aps" : { "mutable-content" : 1 } override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else { contentHandler(request.content); return } mutableContent.subtitle = "Got it!" contentHandler(mutableContent) }
Por exemplo, você pode enviar um link para o conteúdo da mídia na notificação, fazer o download do conteúdo na Extensão e anexar o download à notificação. Depois disso, conclua a chamada com um novo contexto e mostre ao usuário uma notificação por push estendida. Você pode alterar o título, legenda etc.
Outro caso interessante é que você pode enviar uma notificação por push com um contexto criptografado, se desejar que os dados sejam protegidos adicionalmente e a Apple não os tenha visto. Na extensão de conteúdo da notificação, você pode descriptografá-los e mostrar ao usuário os dados já descriptografados.
Conteúdo de notificação oculta
No iOS 11, tornou-se possível ocultar o conteúdo das notificações por push, e nós, como desenvolvedores, não podemos influenciar isso de forma alguma. Se o usuário marcou "Ocultar conteúdo da notificação", de uma forma ou de outra, ele ficará oculto. Tudo o que podemos fazer é através da UNNotificationCategory para especificar um espaço reservado que será exibido em vez do conteúdo (por padrão, essa é a notificação) e definir se o título ou a legenda será exibido.
let commentCategory = UNNotificationCategory(identifier: "comment-category", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey:"COMMENT_KEY",arguments: nil), options: [.hiddenPreviewsShowTitle])
Etapas de notificação sem iniciar o aplicativo
Para executar ações de notificação por push sem iniciar o próprio aplicativo, você precisa criar uma categoria e adicionar ação a ela. O identificador de categoria é passado para o campo de categoria da notificação de carga útil. Você pode conectar ações diferentes a diferentes tipos de notificações.
"aps" : { "category" : "message" } let action = UNNotificationAction(identifier:"reply", title:"Reply", options:[]) let category = UNNotificationCategory(identifier: "message", actions: [action], minimalActions: [action], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([category])
Notificações ricas
Nesta extensão, você pode processar ações adicionais adicionadas à notificação por push e exibir a interface do usuário personalizada.
Para fazer isso, você precisa adicionar a extensão de conteúdo de notificação ao aplicativo, definir uma classe que herda de UNNotificationContentExtension e trabalhar com ela como em um UIViewController regular.
class NotificationViewController: UIViewController, UNNotificationContentExtension { @IBOutlet var userLabel: UILabel? func didReceive(_ notification: UNNotification) { let content = notification.request.content self.title = content.title let userInfo = content.userInfo self.userLabel?.text = userInfo["video-user"] as? String } }
Se você estiver processando ações personalizadas, é importante lembrar que vale a pena atualizar essas ações que você está mostrando ao usuário. Não é necessário tentar implementar a lógica de negócios nesta extensão. Envie uma solicitação ao servidor por ação com uma notificação por push no aplicativo principal e não aqui. Este local é apenas para interface do usuário.
Esquema de entrega de notificação por push
Veja o quanto você pode fazer com as notificações por push no iOS. De versão para versão, temos cada vez mais novas funcionalidades, mas o esquema de entrega de notificações por push agora é exatamente o mesmo que no iOS 3.

Alguém poderia pensar que o esquema de entrega de notificações por push foi bom desde o início, mas não é.
Existem três nós principais no esquema de entrega de notificações por push:
- provedor que gera notificações push de carga útil;
- APNs - Apple Push Notification Service, que entrega uma notificação;
- dispositivo iOS e seu aplicativo.
Vou pular a parte sobre como se registrar, receber um token, para onde enviá-lo. Suponha que tenhamos tudo isso. O que acontece depois?
- O provedor gera uma carga útil e a envia para os APNs.
- APNs envia para o dispositivo.
- O usuário vê uma mensagem push em seu dispositivo.
O Mail e muitos outros aplicativos usam um esquema avançado de entrega de notificações por push. É adicionada a extensão do serviço de notificação, que recebe notificações push com
"mutable-content" : 1
. O provedor é dividido em um servidor que lida com a lógica de back-end do aplicativo e no próprio provedor, que gera carga útil e lida com assinaturas.
No Yandex, o provedor que forma a carga útil é chamado XIVA. XIVA é um banco de dados de assinatura. O Mail usa o XIVA para trabalhar com notificações por push como uma biblioteca de terceiros.
No Mail, o trabalho com assinaturas é organizado de maneira não trivial. Não assinamos apenas o pedido de notificações, temos várias contas. Podemos assinar contas diferentes ou, dentro de uma conta, escolher quais pastas o usuário deseja receber notificações e quais não deseja. XIVA lida com tudo isso. Alguns outros serviços Yandex também funcionam com o XIVA: todas as informações sobre aplicativos, notificações, assinaturas e tokens são armazenadas no XIVA.
Onde estão as perdas?
Existem quatro setas no esquema de entrega de notificações por push; perdas podem ocorrer em três dessas transições.
Entre o servidor e o XIVA, podem ocorrer perdas no seguinte caso. O usuário recebeu uma carta, o servidor sabe disso, gera uma notificação e a envia para o XIVA. Mas o XIVA pode perder essas informações, por exemplo, se um usuário no aplicativo escolher "Inscrever-se" em uma pasta específica enquanto estiver offline. O XIVA não receberá informações sobre a assinatura da pasta e, quando chegar a carga, ela será excluída e o usuário não verá a notificação.
Entre XIVA e APNs , pode ocorrer perda de rede. Dificilmente podemos afetar a rede, por isso não vamos nos deter neste ponto.
Entre APNs e extensão, ou APNS e iOS, se você não estiver usando a extensão. Este é o tipo mais comum de perda. Essas perdas ocorrem porque os APNs não armazenam mais de um push por aplicativo no dispositivo. Se, enquanto o usuário estiver offline, ele receber várias notificações, quando ficar online, verá apenas a última mensagem.
Essas são as mesmas perdas que não nos permitem garantir a entrega e dependemos de notificações por push. A Apple escreve claramente que a entrega não é garantida.
Entre o aplicativo Extension e o iOS, não podem ocorrer perdas , e a Apple garante isso. Se você usar a Extensão e substituir o método didReceiveContent pelo de conclusão, mesmo se você não chamar essa conclusão, a notificação será mostrada assim mesmo. Isso é importante lembrar. Você não pode chamá-lo ou não tem tempo para chamá-lo, mas a notificação será exibida sem nenhuma alteração, na forma em que vem dos APNs.
Veremos como lidamos com as perdas entre APNs e Extensão. Mas se você precisar aumentar a capacidade de entrega de notificações por push, dê uma olhada em todo o esquema. Verifique se há alguma perda no lado do serviço, se o seu provedor interage normalmente com os APNs e assim por diante. Verifique e meça toda a cadeia e, em seguida, tire conclusões onde as perdas ocorrem mais e qual parte deste circuito deve ser modificada.
Fila de notificação por push
Nossa maneira de lidar com as perdas no pacote de APNs e extensões chamamos de fila de notificação por push.
Se você compactar toda a história em uma frase, será:
Se você perdeu a notificação por push, pode solicitá-la novamente.

Em nosso esquema de entrega de notificações, todos os mesmos participantes são: XIVA, APNs, Extensão. Esquema simplificado funciona assim:
- O XIVA numera as notificações por push que pretende enviar para os APNs e, somente então, envia as informações.
- O ramal recebe um número de notificação por push 1 e, após algum tempo, o número 3. Ele entende que alguns dados estão ausentes.
- Envia para o XIVA uma solicitação com a última posição recebida, diff e solicita o envio dos dados ausentes novamente.
- O XIVA reenvia a notificação por push porque armazena o banco de dados de cargas e o banco de dados de assinatura. Todas as assinaturas são armazenadas por algum tempo e podem ser solicitadas novamente.
- Solicitamos novamente, recebemos uma notificação por push e temos no cliente todas as mensagens que o cliente deveria ter recebido.
O primeiro problema esperado é notificações duplicadas. Quando solicitamos uma mensagem da XIVA, não sabemos o que está na fila para o envio, porque nos comunicamos com ela não diretamente, mas por meio de APNs. Suponha que vimos que algumas notificações estavam ausentes e enviamos uma solicitação ao XIVA. XIVA enviado via APNs de carga útil com uma notificação perdida. Mas antes de recebê-lo, recebemos outra carga útil e também com um passe. Eles perguntaram novamente - XIVA enviou novamente.
Para que as notificações não sejam duplicadas, usamos
apns-collapse-id . Essa configuração permite que o lado do iOS recolha notificações push com o mesmo ID. Se várias notificações push com o mesmo apns-collapse-id tiverem chegado ao dispositivo, o iOS as recolherá e o usuário verá apenas uma notificação.
XIVA
Vou lhe contar como tudo funciona no XIVA, porque é sempre curioso o que acontece no back-end.
O XIVA existia antes da fila de notificações por push e era um banco de dados de assinatura. É importante que no banco de dados tudo tenha sido armazenado pelos usuários:
- A chave era
<service, user>
. - A carga útil foi armazenada como valor (dados sobre cartas no caso do Mail).
O XIVA pegou os dados do banco de dados e os enviou para os APNs ou outro serviço, pois funciona não apenas com o iOS. Decidimos reutilizá-lo.
Viemos para a equipe de desenvolvimento do XIVA e realmente pedimos uma fila de notificações por push. Em princípio, o XIVA já tinha tudo para isso: o banco de dados, TTL para cargas úteis, ou seja, eles não são excluídos imediatamente, podem ser encaminhados. A única coisa que faltava era que era possível configurar a fila de notificações por push como parte da implementação atual do XIVA - é a numeração de ponta a ponta.
Para numeração de passagem, as notificações por push devem ser numeradas por dispositivo e app_name. Ou seja, a numeração de ponta a ponta é necessária para um dispositivo específico e para um aplicativo específico, a fim de contar com ele no lado do cliente. Fizemos o seguinte: reutilizamos o banco de dados XIVA, mas começamos a gravar cargas úteis usando uma chave diferente. Agora apns_queue atua como um serviço,
device_id + app_name
como usuário - os mesmos dados que precisam ser numerados no cliente, ou seja,
key: <apns_queue, device_id + app_name>
.
Agora, o XIVA pega os dados do banco de dados principal e os coloca na fila quando precisa ser enviado. Nesse momento, as cargas recebem uma nova numeração, porque agora estão no mesmo banco de dados, mas com uma chave diferente. Já a partir de então o XIVA os retira e os envia pelos APNs. No total, o cliente recebe a numeração da carga útil necessária.
O cliente usa a extensão do serviço de notificação.
public override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
didReceive
método
didReceive
e vemos o que veio do servidor. Nós adicionamos
"mutable-content" : 1
a todas as notificações por push para que elas se enquadram na Extensão, pois, caso contrário, não podemos levar em consideração nos cálculos.
Além disso, no código dentro do método, há verificações contínuas: se a carga útil necessária veio, se eles poderiam analisá-la. Se não for analisado, essa mensagem não será do XIVA. Se a mensagem não for da XIVA, não podemos continuar trabalhando com ela e simplesmente concluir a notificação com a notificação de APNs, não realizamos cálculos.
guard let payload = try? self.payloadParser.parsePayload(from: request.content.userInfo) else {
Registramos, verificamos se o deviceId mudou, pois sabemos que no iOS é possível. Honestamente, não encontramos uma alteração no deviceId, mas apenas no caso de estarmos processando, porque se ela mudar, não poderemos confiar nos números do XIVA.
self.logger.logNotificationReceived(with: payload) if lastPositionDeviceId != deviceId {
Além disso, examinamos se podemos receber os dados XIVA nessa carga, sejam eles ou não. Caso contrário, chame contentHandler novamente.
guard let xivaInfo = payload.xivaInfo else { contentHandler(request.content); return }
Se houver dados, verifique se o deviceId recebeu dados. O XIVA envia um hash do dispositivo para a carga, se for verificado e corresponder, continuamos, não, chamamos contentHandler.
guard isHashCompatible(deviceId: deviceId, deviceIdHash: xivaInfo.deviceIdHash) else {
O próximo bloco é para ver se há uma posição salva:
- Se não temos a última posição salva, ainda não recebemos notificações e não entramos no ramal ou, por algum motivo, desistimos. Então não há nada a ser feito para encontrar a diferença perdida, e novamente chamamos de conclusão.
- Se houver, siga em frente.
guard let lastPos = lastNotificationPosition else {
Contamos o número de notificações perdidas. Se falta zero é bom, não perdemos nada.
let missedMessages = xivaInfo.notificationPosition - lastPos - 1 guard missedMessages > 0 else {
Caso contrário, extraímos do XIVA os dados de posição - dessa mesma numeração contínua. Além disso, analisamos se a quantidade de perda não excede um determinado valor definido.
lastNotificationPosition = xivaInfo.notificationPosition guard missedMessages <= repeatMaxCount else {
Por que isso é necessário? Suponha que o usuário esteja offline por um longo tempo e, durante esse período, uma centena de mensagens tenha sido acumulada. Solicitaremos centenas inteiras (é fácil para nós), o XIVA enviará centenas inteiras e o usuário receberá todas as notificações. Mesmo se os agruparmos por ID de thread (e os agruparmos), do mesmo modo, para cada notificação, essa Extensão será chamada, todas as verificações serão aprovadas. Parece improvável que o usuário precise de todas as cem notificações. Portanto, geramos uma notificação na qual escrevemos que você tem 100 mensagens perdidas, acessa o aplicativo e olha. E mostramos ao usuário exatamente esta mensagem, porque podemos substituir as notificações por push.
Quando todas as verificações são aprovadas, enviamos uma solicitação à XIVA: a última posição que chegou até nós e o número de mensagens perdidas. E olha:
- Se o XIVA respondeu com êxito: “Está tudo bem, enviarei os dados”, mostramos ao usuário a notificação atual e aguardamos até que o XIVA envie todo o resto e o usuário veja todas as mensagens perdidas.
- Se o XIVA responder com um erro, mostramos ao usuário uma notificação personalizada de que ele perdeu mensagens que podem ser exibidas no aplicativo.
self.requestMissedNotifications(lastPosition: xivaInfo.notificationPosition, gap: missedMessages) { result in result.onValue { _ in self.logger.logNotificationProcessed(with: .success) contentHandler(request.content) }.onError { error in self.logger.logNotificationProcessed(with: .failure(error)) contentHandler(buildNewNotification()) } }
Assim, a implementação no cliente se resume a um grande número de verificações nas quais descobrimos se podemos trabalhar com os dados recebidos.
Log e outras dificuldades
Como você sabe, para garantir que a abordagem funcione bem, é necessário fazer logon. Começamos a coletar estatísticas sobre um novo método para entregar notificações e comparar como a capacidade de entrega mudou.
Limitações da extensão push
A primeira coisa que encontramos são as restrições de extensão push.
Nem sempre é chamado . Se você desativar o desenho da notificação nas configurações do aplicativo (a capacidade de receber uma notificação permanece ativada, mas todas as renderizações possíveis estão desativadas), o Extension não será chamado - toda a lógica com recontagens e, mais importante, o log não serão chamados. Não conseguiremos descobrir o que é mais importante para nós - se o usuário recebeu uma notificação.
A extensão push tem um limite de tempo . A documentação da Apple diz que em cerca de 30 segundos você precisará concluir a chamada com uma notificação modificada, caso contrário, a notificação inicial será exibida.
Eu me pergunto como descobrimos isso. Implementamos um recurso que chamamos de notificações push “bonitas”, anexamos elementos de mídia às notificações, alteramos o título, a legenda. Durante o teste, algumas notificações por push ficaram lindas, enquanto o restante como patinhos feios permaneceu.
Começamos a observar a diferença entre essas notificações push e descobrimos que não havia diferença, apenas para algumas que conseguimos chamar de conclusão, mas para outras não. , , push- , APNs.
— . Apple , , push-extension, , , . , 12 .
Apple Developer Forum , , . , — 10 .
, . AppMetrica. , AppMetrica , Extension . , - .
: Extension .
push-extension UserDefaults. , , AppMetrica.
. . , , . , . , XIVA ( ), , .
, Notification Extension iOS 10 , Extension, , .
AppMetrica : , push-extension . AppMetrica push-, , . ,
AppMetrica Push SDK .
, . — , . , .

— , , .
, push-, , — .
, , . , …
: , , . , ? - , push-? , ? user experience ?
, 2–3–20 ?
, , , , , , , . , push-. , .
Sumário
Push- iOS . , .. , .
push- ( ) . . XIVA. , , . , , . !
push-extension. , . , .
, . , , , , - . , push- . , , , App Store, , !
AppsConf , 21 22 , .. 50 , . 1 , — .