Arquitetura Outbox: Projetada para evitar que mensagens sejam derramadas
Esta página é uma tradução automática do original em japonês. Ler o original em japonês
13 de fevereiro de 2026
Tarefa: Para evitar o derramamento de mensagens, mesmo que você as limpe imediatamente
No SimpleMemo, a interface do usuário é limpa no momento em que o usuário toca no botão enviar e pode começar imediatamente a escrever a próxima nota. Esta é a UX que é a raiz do estilo Captio.
No entanto, a rede real tem uma latência de 300ms~2 segundos, pode falhar e pode estar offline em primeiro lugar. Muitos aplicativos aguardam a resposta da rede antes de limpar a interface do usuário. É seguro, mas o UX é ruim. Pelo contrário, se você limpar imediatamente, há o risco de perder a mensagem.
O padrão Outbox resolve esse requisito conflitante ao mesmo tempo. A mensagem é armazenada permanentemente no armazenamento criptografado local (Outbox) no momento do toque de transmissão, a interface do usuário é imediatamente apagada e a transmissão de rede é realizada em segundo plano. Se a transmissão for bem-sucedida, exclua-a do Outbox e, se falhar, ela tentará novamente automaticamente. Como resultado, deixa a mensagem não transmitida no terminal e a reenvia, e a latência da experiência está próxima de zero.
Fluxo de design de padrão Outbox
O seguinte é todo o fluxo desde tocar no botão enviar até que a mensagem seja enviada.
- Toque em transmissão → Primeiro, criptografe AES-GCM para Outbox e salve-o permanentemente
No momento em que o usuário toca no botão enviar, a mensagem é criptografada com AES-GCM 256bit e armazenada permanentemente no armazenamento local do dispositivo. A interface do usuário não será apagada até que esta etapa seja concluída. Em outras palavras, o armazenamento Outbox será o primeiro quebra-mar a evitar derramamentos. - Limpe a interface do usuário imediatamente (o usuário pode começar a escrever a seguinte nota)
Outbox Quando o salvamento for bem-sucedido, limpe a visualização de texto e reproduza a animação de transmissão. Do ponto de vista do usuário, é sinônimo de "envisado". Não estou esperando por nenhuma resposta da rede. O objetivo do Send-to-Reset é ser curto sem sentir espera (0.25 segundos, incluindo animação). - Envie um e-mail via Relay API em segundo planoEm paralelo com a limpeza
UI, o SendManager envia uma solicitação HTTP para o Relay API em Trabalhadores Cloudflare. Este processo é completamente assíncrono e não bloqueia o segmento principal. - Se for bem-sucedido, exclua do Outbox
Relay Se você receber uma resposta 200 do API, exclua com segurança a mensagem correspondente do Outbox. Isso completa o ciclo de vida da mensagem. - Se falhar, será reenviado automaticamente
No caso de um erro de rede ou erro de servidor, deixe-o aguardando transmissão, quando a linha for retornada, quando o aplicativo for aberto, processamento em segundo plano (BGTaskScheduler. O momento da execução é decidido por iOS) e reenviá-lo. Há um limite superior no número de vezes para reenviar automaticamente. - Se estiver offline, o NWPathMonitor detecta o retorno da linha e a reenvia automaticamente Se você estiver offline no momento do envio do
, o NWPathMonitor do Network.framework monitora o status da linha e, quando detecta o retorno do Wi-Fi ou celular, ele reenvia automaticamente a mensagem não transmitida do Outbox (como no 5 acima, há um limite superior no número de reenvios automáticos).
Código do Gerenciador de envio
A função send() do SendManager é o ponto de entrada do padrão Outbox. Primeiro, salve-o no Outbox, faça fila se não houver conexão de rede e execute a transmissão imediatamente se houver uma conexão.
// 保存完了と送信結果を分ける説明用の簡略API
@MainActor
func send(message: String, onSaved: () -> Void,
completion: @escaping (SendResult) -> Void) {
// まずOutboxに保存(クラッシュや圏外で失わないように)
let outboxMessage: OutboxMessage
do {
outboxMessage = try OutboxManager.shared.add(body: message)
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
// 同期保存に成功した場合だけ、メインスレッドでUIへ通知する
onSaved()
// ネットワーク接続がない場合はキューに入れたことを通知
guard NetworkMonitor.shared.isConnected else {
BackgroundTaskManager.shared.scheduleRetryTask()
DispatchQueue.main.async { completion(.queued) }
return
}
// 送信処理を実行
performSend(message: message, outboxId: outboxMessage.id, ...)
}
O ponto importante é que o OutboxManager.shared.add(body:) grava no armazenamento persistente de forma síncrona. Quando esta chamada é bem-sucedida, a mensagem é armazenada com segurança no terminal. Não importa qual seja o resultado da transmissão de rede, a mensagem permanecerá no Outbox.
Implementação da criptografia AES-GCM
Mensagens armazenadas no Outbox são notas privadas para os usuários. Embora seja um armazenamento local, não pode ser armazenado em texto simples. Usando o AES-GCM do Apple CryptoKit, é uma implementação que não usa nenhum SDK externo para criptografia.
- Apple CryptoKit — Disponível para iOS 13 e posterior. Não use SDK externo para processamento de criptografia (somente Apple genuíno).
- Chave simétrica 256-bit — Salvar nas Chaves.
- Criptografia com autenticação — AES-GCM garante a confidencialidade e a integridade dos dados ao mesmo tempo. Dados adulterados são detectados no momento da descriptografia.
enum OutboxEncryption {
private static var encryptionKey: SymmetricKey {
if let existingKey = KeychainHelper.load(key: "outbox_encryption_key") {
return SymmetricKey(data: existingKey)
}
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
KeychainHelper.save(key: "outbox_encryption_key", data: keyData)
return newKey
}
static func encrypt(_ plainText: String) throws -> Data {
guard let data = plainText.data(using: .utf8) else {
throw OutboxError.encryptionFailed
}
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
guard let combined = sealedBox.combined else {
throw OutboxError.encryptionFailed
}
return combined
}
}
A chave é criada automaticamente quando necessário pela primeira vez e armazenada no Keychain. A configuração de salvamento é "apenas este dispositivo, lido após o primeiro desbloqueio" (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), que não pode ser lido de outros aplicativos e não se move do backup para outro terminal. Se você desbloqueá-lo uma vez após a reinicialização, também poderá lê-lo no processamento em segundo plano durante o bloqueio. Além disso, o exemplo de código acima é uma versão simplificada no momento da redação do artigo, e o código atual não cria uma nova chave, tratando-a como "sem chave" quando o chaveiro não lê.
O significado de configurar o caminho de dados apenas com Apple genuíno
Todas as estruturas usadas na arquitetura Outbox são nativas da Apple.
- CryptoKit — Criptografia AES-GCM. Nenhuma biblioteca de criptografia de terceiros é necessária.
- Network.framework — Monitoramento do status da linha com o NWPathMonitor. A biblioteca Reachability não é necessária.
- BackgroundTasks — Retentação de fundo por BGTaskScheduler.
O fato de não haver biblioteca externa no caminho de espera tem os seguintes significados.
- Não afetado por atualizações externas — O processamento de aguardar a transmissão não para de funcionar devido à atualização ou encerramento da biblioteca de terceiros.
- Segurança - O código de terceiros não entra no processamento (criptografia, armazenamento, transmissão) que toca o corpo do memorando que aguarda para ser enviado.
Além disso, todo o aplicativo também usa um SDK externo (Google sign-in, Firebase, AppsFlyer) (consulte Privacy First Design). O "apenas genuíno Apple" mencionado aqui é sobre o caminho dos dados que aguardam para serem transmitidos.
Separação da limpeza da interface do usuário e transmissão de rede
O valor verdadeiro do padrão Outbox está na separação completa da camada de interface do usuário e da camada de rede. Vamos dar uma olhada no manipulador de toque do botão de envio.
@objc private func sendButtonTapped() {
PerformanceLogger.shared.beginSendToReset()
let message = textView.text ?? ""
isSending = true
sendBarButton.isEnabled = false
SendManager.shared.send(message: message, onSaved: { [weak self] in
// 端末への保存成功後にだけ消す。ネットワーク応答は待たない。
self?.performSendAnimation { [weak self] in
guard let self = self else { return }
// アニメーション中に編集された入力は残す。
if self.textView.text == message {
self.clearTextView()
}
PerformanceLogger.shared.endSendToReset()
}
}) { [weak self] result in
self?.isSending = false
self?.updateSendButtonState()
switch result {
case .success:
PerformanceLogger.shared.logEvent(name: "SendSuccess")
case .failure(let error):
self?.handleSendError(error)
case .queued:
self?.showQueuedFeedback()
}
}
}
Neste exemplo simplificado, onSaved é chamado somente após salvar com sucesso no dispositivo; a partir daí, a animação é executada e o texto digitado é apagado. Se o salvamento falhar, o texto é mantido. completion é uma notificação separada sobre o resultado do envio ou a inclusão na fila; a limpeza da interface não espera uma resposta da rede.
O objetivo do Send-to-Reset é shortness que não parece esperar. 0.25 segundos incluindo animação. Imediatamente após tocar no botão enviar, o usuário pode começar a escrever a próxima nota. Este é o estilo Captio UX.
Perguntas frequentes
P. O que acontece com as notas enviadas offline?
É criptografado e salvo pelo Outbox e é reenviado automaticamente quando a linha é restaurada (detectada pelo NWPathMonitor), quando o aplicativo é aberto e o processamento em segundo plano é acionado. Há um limite superior no número de vezes para reenviar automaticamente e, para notas que atingiram o limite superior, pressione e segure em "Histórico" e selecione "Reenviar".
Q. As notas não serão perdidas mesmo se eu fechar o aplicativo?
Outbox é um armazenamento permanente. Mesmo que você feche o aplicativo ou reinicie o iPhone, ele permanecerá. Na próxima vez que o aplicativo for iniciado, ou a tarefa em segundo plano será retentada automaticamente.
Q. Por que você escolheu o AES-GCM?
Apple CryptoKit é suportado nativamente, e a detecção de adulteração também é possível com Criptografia Autenticada. O fato de não haver necessidade de uma biblioteca externa para processar criptografia também é uma vantagem.
P. Qual é o número de novas tentativas em caso de falha de transmissão?
Será enviado novamente automaticamente. O gatilho é o processamento em segundo plano quando a linha é retornada, quando o aplicativo é aberto (BGTaskScheduler. O tempo de execução é determinado por iOS). Os memorandos que não puderam ser enviados serão enviados novamente sem excluí-los da espera de transmissão, mas há um limite para o número de vezes que o reenvio automático.
Artigos relacionados
Referências
- Apple Desenvolvedor — AES.GCM (CryptoKit) — Outbox Referência Oficial para Criptografia de Autenticação AES-GCM Usada para Criptografar Dados
- Apple Desenvolvedor — NWPathMonitor (Network Framework) — Usado para detecção offline e monitoramento de status da rede
- Apple Desenvolvedor — BGTaskScheduler — Usado para reenviar o agendamento de processamento em segundo plano
- Chris Richardson — Padrão: Caixa de saída transacional (microservices.io) — Explanação do padrão transacional Outbox (escreva a mensagem a ser enviada na mesa de espera no mesmo processo, e outro processo é enviado)