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 da 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 registadas, da unidade de distância, dos adiamentos escolhidos pelo utilizador e da previsão atual. A despesa mensal tem outras regras, incluindo converter cada registo à taxa da sua própria data quando foi pago noutra moeda.
Estas respostas já existiam em TypeScript. Adicionar widgets iOS seria uma boa oportunidade para as copiar para Swift e passar o ano seguinte a corrigir divergências entre as duas versões.
Em vez disso, a 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 corre num processo separado. Não executa React Native nem abre o armazenamento MMKV da app.
A fronteira entre os dois processos é um snapshot JSON num contentor App Group. A app TypeScript constrói-o a partir dos mesmos módulos que alimentam o painel. O Swift descodifica-o e apresenta o resultado.
Partilhar respostas prontas entre processos
O snapshot é um contrato de apresentação, não uma exportação de registos em bruto. 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 ficheiro 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 descodificador. Quando o significado de um campo muda, a versão do snapshot também muda. Um descodificador que não reconheça essa versão pede ao utilizador que abra a 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 a app
O calendário é a exceção intencional às respostas pré-calculadas. Um widget pode continuar visível durante dias sem que a app seja aberta. Um prazo que diz «daqui a três dias» tem de mudar depois da meia-noite sem pedir ao React Native que o reconstrua.
O snapshot guarda, por isso, o prazo como dia do calendário local e inclui um dia warningFrom. Guarda ainda 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 da 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 conta-quilómetros.
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. Isto fornece ao WidgetKit estados futuros de apresentação, mas não promete executar a extensão num 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 ficheiro também faz parte do desenho. A escrita principal ocorre quando a app sai do primeiro plano, perto do momento em que o utilizador regressa ao ecrã 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 registos não disponibiliza uma subscrição para todas as escritas. Assim, um registo 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 num veículo partilhado que a app ainda não recebeu.
Neste percurso do ciclo de vida, o escritor usa operações síncronas sobre ficheiros. Grava o JSON completo com um nome temporário e depois move-o para o nome definitivo com substituição ativa. Isto mantém o percurso normal de descodificação afastado de documentos parcialmente escritos. Se uma tentativa anterior foi interrompida, o ficheiro 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 a depender 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 na app. Quando o bloqueio biométrico está ativo, o snapshot não contém valores de conta-quilómetros nem montantes. Esses campos ficam a null; os valores sensíveis não são serializados para depois serem escondidos na vista. A matrícula nunca é incluída, independentemente do bloqueio. Prazos e informação de manutenção continuam disponíveis.
As vistas Swift também marcam valores sensíveis para ocultação pelo sistema. Isto suporta os controlos de apresentação do iOS, enquanto o construtor decide quais os valores que podem sequer chegar à extensão.
Terminar sessão e eliminar a conta removem o snapshot e pedem uma atualização. Apagar o ficheiro faz parte de terminar a sessão, não é apenas um efeito secundário 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, guardado na pasta Documents da app. Estes intents são compilados no target da app, não na extensão WidgetKit. Os construtores partilham dados de entrada, mas ficheiros 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 a aplicar o papel na partilha, os limites do plano, as verificações do conta-quilómetros 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 da 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 ficheiros 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 têm de 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 saltava 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 a 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 descodificador Swift, deep links, tabelas de tradução, cores e fontes partilhadas 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 num dispositivo. Detetam várias formas de os consumidores compilados separadamente divergirem antes de a divergência chegar ao ecrã principal.
É uma fronteira que reutilizaria noutro produto React Native: deixar a 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.