Artículo

Crear widgets nativos de iOS con Expo managed

Cómo publiqué widgets nativos de iOS en Expo managed con Config Plugins, App Groups y almacenamiento local-first compartido, sin configurar Xcode manualmente.

Crear widgets nativos de iOS con Expo managed

Al crear una app de utilidad moderna, ya no basta con tener una interfaz preciosa dentro de la aplicación. La verdadera clave para mantener a los usuarios está en la pantalla de inicio.

Cuando desarrollaba WaterPaw, mi app minimalista para controlar la hidratación, los widgets nativos de iOS eran imprescindibles. Los usuarios debían poder consultar de un vistazo sus anillos de hidratación en la pantalla de inicio. Pero si trabajas con React Native y Expo, conoces la dificultad histórica: los widgets requieren Swift nativo, configurar extensiones de app y administrar objetivos de firma de código en Xcode. Hacerlo manualmente rompe por completo el flujo managed de Expo.

Esta es la arquitectura automatizada y sencilla que usé para publicar widgets nativos de iOS con Expo Config Plugins y almacenamiento local-first compartido.

El reto arquitectónico: el aislamiento entre la app y el widget

Como WaterPaw es local-first y prioriza la privacidad, todos los datos permanecen en el dispositivo. Sin embargo, un widget de iOS se ejecuta en su propio proceso en segundo plano y no puede leer el AsyncStorage estándar de la app ni sus archivos del directorio de documentos.

Para conectar ambos sin crear un backend en la nube, hay que configurar un contenedor aislado de Apple llamado App Group:

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

Paso 1: Enviar datos desde React Native

Cuando el usuario registra agua en la capa JavaScript, debemos enviar esos datos al App Group compartido. Podemos usar react-native-shared-group-preferences para gestionar la conexión 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);
  }
};

Paso 2: Leer los datos compartidos desde Swift

Desde el directorio nativo del widget en Swift, puedes leer al instante esa misma suite de App Group mediante UserDefaults(suiteName:). Así todo permanece local, es inmediato y 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()
    }
}

Paso 3: Automatizar Xcode con Expo Config Plugins 🛠️

Aquí suelen surgir los problemas. Si ejecutas npx expo prebuild, Expo eliminará cualquier modificación manual de Xcode y los objetivos que hayas configurado para el widget.

Para mantener un flujo totalmente managed, aislamos el código Swift en un directorio targets/ independiente y usamos un Config Plugin, como expo-target-extension, para automatizar el ensamblado nativo. Así Expo crea automáticamente el objetivo del widget, enlaza las fases de compilación de Swift e incorpora los permisos de App Group cada vez:

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

Al guardar esta configuración en app.json, borrar el directorio ios/ y ejecutar npx expo prebuild --clean, el entorno nativo de Xcode queda completamente configurado.

Conclusión: automatiza en lugar de salir de Expo

Trasladar por completo los flujos de extensiones nativas a Config Plugins mantiene el proyecto móvil limpio y preparado para el futuro. Conservas la rapidez de iteración de Expo y ofreces a los usuarios una función del sistema iOS bien integrada y sin latencia.

En las apps de utilidad, el esfuerzo adicional para mostrar los datos de un vistazo en la pantalla de inicio mejora mucho la participación a largo plazo. Usé esta arquitectura para implementar los anillos de progreso y el seguimiento de la mascota de WaterPaw.