Artikel

Native iOS-Widgets mit Managed Expo entwickeln

Wie ich native iOS-Widgets mit Config Plugins, App Groups und gemeinsamem Local-First-Speicher in Managed Expo veröffentlicht habe – ohne manuelle Xcode-Konfiguration.

Native iOS-Widgets mit Managed Expo entwickeln

Bei einer modernen Utility-App reicht eine großartige Oberfläche innerhalb der App nicht mehr aus. Der entscheidende Ort für langfristige Nutzung ist der Home-Bildschirm.

Als ich WaterPaw, meinen minimalistischen Trink-Tracker, entwickelte, waren native iOS-Widgets unverzichtbar. Nutzer sollten einen Blick auf den Home-Bildschirm werfen und ihre Trinkfortschritte sehen können. Wer mit React Native und Expo arbeitet, kennt jedoch die Herausforderungen: iOS-Widgets erfordern nativen Swift-Code, eigene App Extensions und Code-Signing-Ziele in Xcode. Manuelle Einrichtung durchbricht den nahtlosen Managed-Expo-Workflow.

So sieht die saubere, automatisierte Architektur aus, mit der ich native iOS-Widgets über Expo Config Plugins und gemeinsamen Local-First-Speicher veröffentlicht habe.

Die Architektur-Herausforderung: die Sandbox zwischen App und Widget

WaterPaw ist Local First und datenschutzorientiert, daher bleiben alle Daten auf dem Gerät. Ein iOS-Widget läuft jedoch in einem eigenen Hintergrundprozess des Betriebssystems und kann weder das normale AsyncStorage der App noch Dateien im Dokumentverzeichnis lesen.

Um diese Lücke ohne Cloud-Backend zu schließen, muss ein Apple-Sandbox-Container namens App Group eingerichtet werden:

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

Schritt 1: Daten aus React Native schreiben

Wenn Nutzer in der JavaScript-Ebene Wasser protokollieren, müssen wir die Daten in die gemeinsame App Group übertragen. react-native-shared-group-preferences kann die native Bridge-Anbindung übernehmen:

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

Schritt 2: Gemeinsame Daten in Swift verwenden

Im nativen Swift-Widget-Verzeichnis kannst du dieselbe App-Group-Suite sofort über UserDefaults(suiteName:) auslesen. So bleibt alles lokal, schnell und zu 100 % privat:

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

Schritt 3: Xcode mit Expo Config Plugins automatisieren 🛠️

Hier geraten Entwickler häufig in Schwierigkeiten. Wenn du npx expo prebuild ausführst, entfernt Expo alle manuellen Xcode-Änderungen und Widget-Ziele.

Für einen vollständig verwalteten Workflow legen wir den Swift-Code in einem eigenen targets/-Verzeichnis ab und nutzen ein Config Plugin wie expo-target-extension, um den nativen Aufbau zu automatisieren. Expo erstellt damit jedes Mal automatisch das Widget-Ziel, verknüpft die Swift-Kompilierungsphasen und fügt die App-Group-Berechtigungen ein:

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

Wenn diese Konfiguration in app.json liegt, das ios/-Verzeichnis gelöscht und npx expo prebuild --clean ausgeführt wird, wird die native Xcode-Umgebung vollständig eingerichtet.

Fazit: Nicht aussteigen, automatisieren

Wenn native Erweiterungs-Workflows vollständig in Config Plugins liegen, bleibt das Mobilprojekt sauber und zukunftssicher. Du behältst die schnellen Entwicklungszyklen von Expo und kannst Nutzern gleichzeitig eine tief integrierte iOS-Systemfunktion ohne Verzögerung bieten.

Bei Utility-Apps zahlt sich der zusätzliche Aufwand, Daten auf dem Home-Bildschirm schnell erfassbar zu machen, langfristig durch mehr Engagement aus. Mit dieser Architektur habe ich die Fortschrittsringe und die Maskottchen-Anzeige in WaterPaw umgesetzt.