Widgets iOS
Widgets em React Native sem duplicar a lógica de negócio em Swift
Um snapshot JSON versionado permite ao WidgetKit reutilizar cálculos TypeScript, atualizar prazos e respeitar a privacidade do app.
Atualizado em
Neste artigo
“Próxima manutenção” é uma legenda curta com uma lista de dependências surpreendentemente longa.
No OdoKeep, pode depender do veículo, das manutenções registradas, da unidade de distância, dos adiamentos escolhidos pelo usuário e da previsão atual. A despesa mensal tem outras regras, incluindo converter cada registro à taxa da sua própria data quando foi pago em outra moeda.
Estas respostas já existiam em TypeScript. Adicionar widgets iOS seria uma boa oportunidade para as copiar para Swift e passar o ano seguinte corrigindo divergências entre as duas versões.
Em vez disso, o app grava as respostas.
O OdoKeep usa React Native e Expo. Tem quatro widgets WidgetKit: detalhes do veículo, prazos, despesa mensal e manutenção. A extensão roda em um processo separado. Não executa React Native nem abre o armazenamento MMKV do app.
A fronteira entre os dois processos é um snapshot JSON em um container App Group. O app TypeScript o constrói a partir dos mesmos módulos que alimentam o painel. O Swift o decodifica e apresenta o resultado.
Compartilhar respostas prontas entre processos
O snapshot é um contrato de apresentação, não uma exportação de registros brutos. Os montantes já estão formatados, as unidades aplicadas, as legendas traduzidas e os deep links construídos. Até os intervalos do gráfico de despesas transportam as legendas finais e as proporções normalizadas das barras.
Este excerto ilustra a forma do contrato:
interface WidgetSpendBucket {
id: string;
label: string;
ratio: number;
isCurrent: boolean;
}
interface WidgetDeadline {
id: string;
label: string;
vehicleId: string;
vehicleName: string;
dueDate: string;
dueDateLabel: string;
warningFrom: string;
url: string;
}
O arquivo conserva estrutura útil. O Swift precisa de identificadores para permitir escolher veículos, de datas para fazer avançar a apresentação e de proporções para desenhar barras. Não precisa de uma segunda implementação das regras financeiras ou de manutenção da garagem.
A lista de alertas, as previsões de manutenção e os cálculos de despesa alimentam lib/widgets/snapshot.ts. plugins/widgets/WidgetsSnapshot.swift espelha o contrato do decodificador. Quando o significado de um campo muda, a versão do snapshot também muda. Um decodificador que não reconheça essa versão pede ao usuário que abra o app.
É uma falha muito mais fácil de investigar do que um widget que apresenta, com aparente certeza, uma interpretação errada de um novo campo.
Deixar o calendário avançar sem abrir o app
O calendário é a exceção intencional às respostas pré-calculadas. Um widget pode continuar visível durante dias sem que o app seja aberto. Um prazo que diz “daqui a três dias” precisa mudar depois da meia-noite sem pedir ao React Native que o reconstrua.
O snapshot salva, por isso, o prazo como dia do calendário local e inclui um dia warningFrom. Também salva frases traduzidas, indexadas pela diferença em dias:
"0" -> Hoje
"1" -> Amanhã
"2" -> Daqui a 2 dias
"-2" -> Atrasado há 2 dias
A tabela real é produzida pela função de tradução do app e cobre sessenta dias no passado e 120 no futuro, com um texto alternativo para atrasos. O Swift conta dias de calendário e procura a frase já preparada. Pode mudar o estado na data de aviso pré-calculada sem conhecer a regra que a determinou.
Nas manutenções definidas pela distância, o snapshot transporta uma legenda fixa. Passar uma noite não acrescenta quilômetros ao hodômetro.
WidgetTimeline.swift cria uma entrada para o momento atual e uma por meia-noite local durante a semana seguinte, com a política de atualização .atEnd. O snapshot permanece igual; muda a data da entrada. Isso fornece ao WidgetKit estados futuros de apresentação, mas não promete executar a extensão em um instante exato. O WidgetKit controla o agendamento, como explica a documentação da Apple sobre atualização de widgets.
Substituir o snapshot antes de pedir a atualização
Escolher quando gravar o arquivo também faz parte do desenho. A escrita principal ocorre quando o app sai do primeiro plano, perto do momento em que o usuário volta à tela principal. Um temporizador de quatro segundos após a montagem fornece um snapshot inicial às sessões que ainda não passaram para segundo plano. Alterações observáveis nas preferências e na configuração rearmam esse temporizador.
O repositório de registros não oferece um mecanismo para observar cada escrita. Assim, um registro adicionado depois de o temporizador disparar só chega ao snapshot na próxima passagem para segundo plano. Esse é o limite de atualidade dos dados. Um widget também não conhece uma edição em um veículo compartilhado que o app ainda não recebeu.
Neste percurso do ciclo de vida, o escritor usa operações síncronas sobre arquivos. Grava o JSON completo com um nome temporário e depois o move para o nome definitivo com substituição ativa. Isso mantém o percurso normal de decodificação afastado de documentos parcialmente escritos. Se uma tentativa anterior foi interrompida, o arquivo temporário é removido antes de ser reutilizado.
Só depois da substituição o pequeno módulo nativo Expo pede a atualização da timeline WidgetKit. Gravar bytes, por si só, deixaria a timeline anterior ativa até o sistema pedir outra. A atualização é um pedido ao agendador: continua dependendo do iOS e de uma escrita bem-sucedida.
A privacidade começa no construtor do snapshot
Os dados que atravessam a fronteira são menos do que os disponíveis no app. Quando o bloqueio biométrico está ativo, o snapshot não contém valores de hodômetro nem montantes. Esses campos ficam a null; os valores sensíveis não são serializados para depois serem ocultados na interface. A placa nunca é incluída, independentemente do bloqueio. Prazos e informação de manutenção continuam disponíveis.
As interfaces Swift também marcam valores sensíveis para ocultação pelo sistema. Isso suporta os controles de apresentação do iOS, enquanto o construtor decide quais os valores que podem sequer chegar à extensão.
Sair da sessão e excluir a conta removem o snapshot e pedem uma atualização. Apagar o arquivo faz parte de encerrar a sessão, não é apenas um efeito colateral de desmontar o hook que o escreve. Caso contrário, o snapshot poderia sobreviver à conta que o produziu.
Reutilizar a fronteira para a Siri e os Atalhos
A mesma escolha de arquitetura serve a Siri e os Atalhos. Os intents de leitura usam outro snapshot de apresentação, salvo na pasta Documents do app. Estes intents são compilados no target do app, não na extensão WidgetKit. Os construtores compartilham dados de entrada, mas arquivos e consumidores têm contratos distintos.
Um atalho de escrita abre um deep link para um formulário existente. Não altera o armazenamento em Swift. O percurso continua aplicando o papel no compartilhamento, os limites do plano, as verificações do hodômetro e a sincronização usados quando o formulário é aberto normalmente. Os parâmetros da Siri preenchem um pedido; não contornam o percurso de escrita do app.
Reproduzir a extensão com Expo prebuild
O build nativo também é reproduzível. O projeto regenera a pasta iOS ignorada pelo Git através do Expo prebuild. Um target adicionado manualmente no Xcode não sobreviveria a uma geração limpa. Por isso, plugins/with-widgets.js cria e configura a extensão, liga os arquivos Swift e os recursos, declara o App Group e descreve o segundo target ao EAS para assinatura. Segue o modelo de config plugins do Expo para alterações que precisam sobreviver à regeneração nativa.
Uma definição merece referência: ENABLE_DEBUG_DYLIB = NO na extensão. A implementação documenta uma falha em que a extração de metadados App Intents inspecionava as dependências do stub de debug, não encontrava AppIntents e pulava a extração sem fazer falhar o build. Os widgets configuráveis ficavam sem um intent utilizável e apresentavam um estado vazio, apesar de o app ter uma sessão ativa. A definição faz a extensão de debug usar a disposição de binários esperada por essa extração.
Os testes verificam o construtor puro do snapshot, os campos obrigatórios do decodificador Swift, deep links, tabelas de tradução, cores e fontes compartilhadas e configuração nativa. São verificações específicas do contrato, não um esquema gerado entre linguagens nem um substituto de executar a extensão em um dispositivo. Detectam várias formas de os consumidores compilados separadamente divergirem antes de a divergência chegar à tela principal.
É uma fronteira que reutilizaria em outro produto React Native: deixar o app definir o significado dos dados e enviar à extensão uma descrição limitada e versionada do que pode apresentar. Conservar estrutura de datas suficiente para a apresentação acompanhar o calendário, e tornar a idade das restantes respostas uma consequência explícita da última escrita.
Estes quatro widgets estão implementados no OdoKeep para iOS. Para quem acompanha um veículo, permitem consultar prazos e a próxima manutenção sem abrir o diário de bordo completo.