Ao criar um app utilitário moderno, uma interface bonita dentro do aplicativo já não é suficiente. A verdadeira disputa pela retenção acontece na tela inicial.
Ao desenvolver o WaterPaw, meu app minimalista de acompanhamento de hidratação, os widgets nativos do iOS eram indispensáveis. Os usuários deveriam poder conferir seus indicadores de hidratação rapidamente na tela inicial. Porém, quem trabalha com React Native e Expo conhece a dificuldade histórica: widgets do iOS exigem Swift nativo, extensões de app personalizadas e configuração de assinatura de código no Xcode. Fazer isso manualmente quebra completamente o fluxo gerenciado do Expo.
Esta é a arquitetura automatizada e simples que usei para publicar widgets nativos do iOS com Expo Config Plugins e armazenamento local-first compartilhado.
O desafio arquitetural: o isolamento entre o app e o widget
Como o WaterPaw é local-first e prioriza a privacidade, os dados ficam inteiramente no dispositivo. Porém, um widget do iOS é executado em seu próprio processo em segundo plano e não pode ler o AsyncStorage padrão do app nem os arquivos do diretório de documentos.
Para conectar os dois sem criar um backend na nuvem, é preciso configurar um contêiner isolado da Apple chamado App Group:
[ WaterPaw React Native App ]
→ (Writes to Shared Suite)
→ [ iOS App Group Container ]
→ [ iOS Native Home Widget ] ← (Reads via Swift)Etapa 1: Enviar dados pelo React Native
Quando o usuário registra água na camada JavaScript, precisamos enviar esses dados ao App Group compartilhado. Podemos usar react-native-shared-group-preferences para fazer a conexão com a bridge nativa:
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const APP_GROUP = 'group.com.yourgroupname';
export const syncDataToWidget = async (currentMl: number, goalMl: number) => {
const dataForWidget = {
progress: currentMl / goalMl,
displayString: `${currentMl} / ${goalMl} ml`,
updatedAt: new Date().toISOString()
};
try {
await SharedGroupPreferences.setItem('WidgetData', dataForWidget, APP_GROUP);
} catch (error) {
console.error('Failed to sync data to iOS App Group Suite', error);
}
};Etapa 2: Ler os dados compartilhados no Swift
No diretório nativo do widget em Swift, você pode ler imediatamente a mesma suite do App Group usando UserDefaults(suiteName:). Assim, tudo permanece local, instantâneo e 100% privado:
import WidgetKit
import SwiftUI
struct HydrationEntry: TimelineEntry {
let date: Date
let progress: Double
let displayString: String
}
struct WaterPawWidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("WaterPaw")
.font(.caption)
.foregroundColor(.secondary)
Text(entry.displayString)
.font(.title2)
.bold()
ProgressView(value: entry.progress)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
}
.padding()
}
}Etapa 3: Automatizar o Xcode com Expo Config Plugins 🛠️
É aqui que os desenvolvedores costumam ter problemas. Ao executar npx expo prebuild, o Expo apaga todas as modificações manuais no Xcode e os targets configurados para o widget.
Para manter um fluxo totalmente gerenciado, isolamos o código Swift em um diretório targets/ separado e usamos um Config Plugin, como expo-target-extension, para automatizar a montagem nativa. Assim, o Expo cria o target do widget, conecta as etapas de compilação do Swift e adiciona as permissões do App Group toda vez:
{
"expo": {
"name": "WaterPaw",
"slug": "waterpaw",
"plugins": [
[
"expo-target-extension",
{
"targetName": "WaterPawWidget",
"dir": "targets/widgets",
"entitlements": {
"com.apple.security.application-groups": ["group.com.yourgroupname"]
}
}
]
]
}
}Ao colocar essa configuração no app.json, limpar o diretório ios/ e executar npx expo prebuild --clean, o ambiente nativo do Xcode fica totalmente configurado.
Conclusão: automatize em vez de sair do Expo
Mover todo o fluxo de extensões nativas para Config Plugins mantém o projeto móvel limpo e preparado para o futuro. Você continua com os ciclos rápidos de desenvolvimento do Expo e oferece ao usuário um recurso do sistema iOS bem integrado e sem latência.
Em apps utilitários, o esforço extra para deixar os dados visíveis rapidamente na tela inicial melhora muito o engajamento a longo prazo. Usei essa arquitetura para implementar os indicadores de progresso e o acompanhamento do mascote no WaterPaw.
