Article

Créer des widgets iOS natifs avec Expo managed

Comment j’ai publié des widgets iOS natifs avec Config Plugins, App Groups et stockage local-first partagé dans Expo managed, sans configuration manuelle de Xcode.

Créer des widgets iOS natifs avec Expo managed

Pour une app utilitaire moderne, une belle interface dans l’application ne suffit plus. Le véritable enjeu de fidélisation se trouve sur l’écran d’accueil.

En développant WaterPaw, mon app minimaliste de suivi de l’hydratation, les widgets iOS natifs étaient indispensables. Les utilisateurs devaient pouvoir consulter leurs anneaux d’hydratation d’un coup d’œil. Mais dans l’écosystème React Native / Expo, cette tâche est réputée difficile : les widgets iOS demandent du Swift natif, des App Extensions personnalisées et la gestion des cibles de signature dans Xcode. Tout faire manuellement casse le workflow Expo managed.

Voici l’architecture automatisée que j’ai utilisée pour publier des widgets iOS natifs avec Expo Config Plugins et un stockage local-first partagé.

Le défi architectural : la sandbox entre l’app et le widget

WaterPaw est local-first et axée sur la confidentialité : les données restent donc sur l’appareil. Un widget iOS s’exécute toutefois dans son propre processus en arrière-plan et ne peut pas lire l’AsyncStorage standard de l’app ni les fichiers de son répertoire documentaire.

Pour faire le lien sans créer de backend cloud, il faut configurer un conteneur sandbox Apple appelé App Group :

[ WaterPaw React Native App ]
    → (Writes to Shared Suite)
        → [ iOS App Group Container ]
            → [ iOS Native Home Widget ] ← (Reads via Swift)

Étape 1 : Écrire les données depuis React Native

Quand l’utilisateur enregistre sa consommation d’eau dans la couche JavaScript, il faut transférer ces données vers l’App Group partagé. react-native-shared-group-preferences permet de gérer la liaison native :

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);
  }
};

Étape 2 : Lire les données partagées en Swift

Dans le répertoire du widget Swift natif, vous pouvez lire instantanément cette même suite App Group avec UserDefaults(suiteName:). Tout reste ainsi local, immédiat et 100 % privé :

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()
    }
}

Étape 3 : Automatiser Xcode avec Expo Config Plugins 🛠️

C’est souvent là que les développeurs rencontrent des difficultés. Si vous lancez npx expo prebuild, Expo supprimera toutes les modifications manuelles de Xcode et les cibles du widget.

Pour conserver un workflow entièrement managed, nous isolons le code Swift dans un dossier targets/ et utilisons un Config Plugin, comme expo-target-extension, pour automatiser l’assemblage natif. Expo crée alors la cible du widget, relie les phases de compilation Swift et ajoute les autorisations App Group à chaque génération :

{
  "expo": {
    "name": "WaterPaw",
    "slug": "waterpaw",
    "plugins": [
      [
        "expo-target-extension",
        {
          "targetName": "WaterPawWidget",
          "dir": "targets/widgets",
          "entitlements": {
            "com.apple.security.application-groups": ["group.com.yourgroupname"]
          }
        }
      ]
    ]
  }
}

En plaçant cette configuration dans app.json, en supprimant le dossier ios/ et en lançant npx expo prebuild --clean, l’environnement natif Xcode est configuré automatiquement.

Conclusion : automatiser plutôt que quitter Expo

En déplaçant entièrement les workflows d’extensions natives dans des Config Plugins, le projet mobile reste propre et prêt pour l’avenir. Vous conservez les cycles d’itération rapides d’Expo tout en offrant une fonctionnalité iOS profondément intégrée et sans latence.

Pour une app utilitaire, rendre les données consultables d’un coup d’œil sur l’écran d’accueil améliore considérablement l’engagement à long terme. J’ai utilisé cette architecture pour les anneaux de progression et le suivi du compagnon dans WaterPaw.