---
title: "Del ecosistema expo-* a los frameworks de Apple: la tabla de equivalencias por dominio"
description: "Qué framework de Apple reemplaza a cada paquete expo-* al pasar de React Native a iOS nativo: cámara, ubicación, autenticación, pagos, notificaciones, archivos y más, con los permisos de Info.plist y el crash silencioso que todos cometen."
author: Ramón Chancay
date: 2026-09-09
lang: es
tags: [Expo, iOS, Swift, Frameworks de Apple, Permisos, Info.plist, React Native]
canonical: https://www.ramonchancay.me/es/blog/del-ecosistema-expo-a-los-frameworks-de-apple
---

# Del ecosistema expo-* a los frameworks de Apple: la tabla de equivalencias por dominio

Expo hizo algo que se nota cuando lo pierdes: envolvió cada capacidad del teléfono en un paquete con la misma forma. `expo-camera`, `expo-location`, `expo-notifications`, `expo-secure-store`: instalar, pedir permiso con una función, usar. En nativo no existe esa uniformidad. Cada capacidad es un framework de Apple con su propia historia, su propia API (a veces en Swift moderno, a veces con delegates de 2010) y su propia forma de pedir permisos. Este post es la tabla que me hubiera gustado tener: por dominio, qué paquete `expo-*` usabas, qué framework lo reemplaza, qué permiso requiere y qué cambia en el uso. Y al final, el error que todos cometemos la primera vez: el crash silencioso por una clave que falta en `Info.plist`.

<details class="rc-tldr">
<summary class="rc-tldr-btn"><span class="rc-tldr-dot"></span> Resumen para perezosos <svg class="rc-tldr-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
<div class="rc-tldr-body">
<ul>
<li>Casi todo paquete <code>expo-*</code> es un envoltorio de un framework de Apple: AVFoundation, CoreLocation, UserNotifications, StoreKit, AuthenticationServices, PhotosUI. Aprender el framework es aprender lo que el paquete escondía, incluidas sus decisiones de diseño.</li>
<li>Cada permiso tiene dos partes: una clave de uso en <code>Info.plist</code> con el texto que ve el usuario, y una llamada de solicitud en tiempo de ejecución. Si falta la clave, la app se cierra sin diálogo y sin error legible; es el crash silencioso de la primera semana.</li>
<li>Para lo que Apple no cubre (analytics de terceros, mapas de Google, ciertos SDK), Swift Package Manager tiene la mayoría de los SDK oficiales. Lo que no existe en SPM es un aviso: probablemente tampoco existe una alternativa madura.</li>
</ul>
</div>
</details>

**En este artículo:**

- **La tabla** — [Cómo leer la tabla](#cómo-leer-la-tabla) · [Cámara, fotos y medios](#cámara-fotos-y-medios) · [Ubicación y mapas](#ubicación-y-mapas) · [Autenticación](#autenticación-y-sesión) · [Pagos y suscripciones](#pagos-y-suscripciones) · [Notificaciones](#notificaciones) · [Dispositivo y sistema](#dispositivo-archivos-y-sistema)
- **Los permisos** — [Info.plist y el crash silencioso](#permisos-infoplist-y-el-crash-silencioso) · [Un patrón para pedirlos](#un-patrón-para-pedir-permisos-sin-repetir-código)
- **Lo que falta** — [Lo que Apple no cubre](#lo-que-apple-no-cubre-y-de-dónde-sale)

## Cómo leer la tabla

Cada fila da el paquete de Expo, el framework de Apple que lo reemplaza, el permiso que hace falta (la clave de `Info.plist` y, si aplica, la capacidad o entitlement del proyecto) y el cambio de uso que más sorprende. No es exhaustiva; es la lista de lo que aparece en la mayoría de las apps comerciales.

Dos aclaraciones generales:

- **"Framework" no significa una sola clase.** `expo-camera` se reemplaza con AVFoundation, que es un framework enorme; para tomar una foto usas cuatro o cinco tipos de él. La tabla apunta al framework, y el texto a la parte que usas.
- **Muchos de estos frameworks tienen dos APIs**: una moderna con `async/await` y otra anterior con delegates o callbacks. Cuando existe la moderna, es la que uso; cuando no, `withCheckedContinuation` o `AsyncStream` la envuelven una vez, como explica el [post de concurrencia](/es/blog/concurrencia-en-swift-adios-al-event-loop).

## Cámara, fotos y medios

| Expo | Apple | Permiso | Lo que cambia |
|---|---|---|---|
| `expo-camera` | AVFoundation (`AVCaptureSession`, `AVCapturePhotoOutput`) | `NSCameraUsageDescription`; `NSMicrophoneUsageDescription` para video | No hay un componente de vista previa: montas una `AVCaptureSession` con dispositivo, entrada y salida, y muestras la vista previa con `AVCaptureVideoPreviewLayer` envuelta en `UIViewRepresentable`. Es más código y control total (enfoque, exposición, formato RAW). |
| `expo-image-picker` (cámara) | `UIImagePickerController` (envuelto) | `NSCameraUsageDescription` | Para "tomar una foto y ya", el picker del sistema sigue siendo la opción corta. Es UIKit, se envuelve una vez. |
| `expo-image-picker` (galería) | PhotosUI (`PhotosPicker`) | Ninguno | `PhotosPicker` es SwiftUI nativo y corre fuera de tu proceso: el usuario elige y tú recibes solo lo elegido, sin pedir permiso de fototeca. Es el cambio más agradable de la tabla. |
| `expo-media-library` | Photos (`PHPhotoLibrary`) | `NSPhotoLibraryUsageDescription`; `NSPhotoLibraryAddUsageDescription` para solo guardar | Solo si necesitas leer la biblioteca completa o guardar en un álbum. Desde iOS 14 el usuario puede dar acceso limitado a fotos seleccionadas, y tu código tiene que soportar ese estado. |
| `expo-av` (reproducción) | AVFoundation (`AVPlayer`), AVKit (`VideoPlayer`) | Ninguno | `VideoPlayer` de AVKit es una vista de SwiftUI con controles del sistema. `AVPlayer` para audio y control programático. Para audio en segundo plano, la capacidad Background Modes con Audio. |
| `expo-av` (grabación) | AVFoundation (`AVAudioRecorder`, `AVAudioSession`) | `NSMicrophoneUsageDescription` | `AVAudioSession` es el concepto que Expo escondía: decide cómo convive tu audio con el del sistema (categoría, modo, interrupciones por llamada). Configurarlo mal es la causa de la mayoría de los bugs de audio. |
| `expo-speech` | AVFoundation (`AVSpeechSynthesizer`) | Ninguno | API directa. Voces del sistema, con calidad mejorada descargable por el usuario. |
| `expo-barcode-scanner` | AVFoundation (`AVCaptureMetadataOutput`) o Vision (`VNDetectBarcodesRequest`) | `NSCameraUsageDescription` | Vision detecta códigos en una imagen ya tomada; `AVCaptureMetadataOutput` los lee en vivo desde la sesión de captura. Ambos sin librerías. |
| `expo-image` | `AsyncImage` (básico) o Nuke / Kingfisher (SPM) | Ninguno | `AsyncImage` no cachea en disco ni hace prefetch. Para una app con imágenes de red, Nuke o Kingfisher son la dependencia estándar. |

## Ubicación y mapas

| Expo | Apple | Permiso | Lo que cambia |
|---|---|---|---|
| `expo-location` | CoreLocation (`CLLocationManager`, `CLLocationUpdate`) | `NSLocationWhenInUseUsageDescription`; `NSLocationAlwaysAndWhenInUseUsageDescription` para segundo plano; capacidad Background Modes con Location | Desde iOS 17, `CLLocationUpdate.liveUpdates()` entrega ubicaciones como `AsyncSequence` sin delegate. El permiso "siempre" se pide en dos pasos (primero "al usar", después "siempre") y el sistema decide cuándo mostrar el segundo diálogo, no tú. |
| `expo-location` (geocoding) | CoreLocation (`CLGeocoder`) | Ninguno | Límites de tasa del servicio de Apple. Para volumen, un servicio propio. |
| `react-native-maps` | MapKit (`Map` de SwiftUI) | Ninguno; `NSLocationWhenInUseUsageDescription` si muestras la ubicación del usuario | `Map` en SwiftUI con anotaciones, overlays y cámara controlable desde iOS 17. Sin API key, sin cuota. Para mapas de Google, el SDK oficial por SPM. |
| Geofencing | CoreLocation (`CLMonitor`) | Permiso "siempre" | `CLMonitor` (iOS 17) reemplaza a las regiones con delegate. Límite de 20 regiones monitoreadas por app. |

## Autenticación y sesión

| Expo | Apple | Permiso / capacidad | Lo que cambia |
|---|---|---|---|
| `expo-apple-authentication` | AuthenticationServices (`SignInWithAppleButton`) | Capacidad Sign in with Apple | Botón nativo de SwiftUI. Apple exige ofrecerlo si ofreces cualquier otro login social. El correo real solo llega la primera vez; hay que guardarlo entonces. |
| `expo-auth-session` (OAuth) | AuthenticationServices (`ASWebAuthenticationSession`) | Ninguno; esquema de URL o Universal Link para el callback | Abre una sesión de Safari aislada para el flujo OAuth y devuelve la URL de callback. Es lo que hacía `expo-auth-session` por debajo. |
| `expo-local-authentication` | LocalAuthentication (`LAContext`) | `NSFaceIDUsageDescription` | `LAContext.evaluatePolicy` con `async`. Face ID requiere la clave; Touch ID no. Si falta la clave y el dispositivo tiene Face ID, crash silencioso. |
| `expo-secure-store` | Security (Keychain) | Capacidad Keychain Sharing solo para compartir con extensiones | Cubierto en el [post de persistencia](/es/blog/persistencia-en-ios-swiftdata-grdb-keychain): sobrevive a la desinstalación. |
| Passkeys | AuthenticationServices (`ASAuthorizationPlatformPublicKeyCredentialProvider`) | Associated Domains con `webcredentials:` | Sin equivalente maduro en Expo. Es una de las capacidades que hoy justifican nativo en apps con login. |
| Autofill de contraseñas | `.textContentType(.username / .password / .oneTimeCode)` | Associated Domains con `webcredentials:` | Un modifier por campo, y el sistema ofrece las credenciales guardadas y los códigos de SMS. |

## Pagos y suscripciones

| Expo | Apple | Permiso / capacidad | Lo que cambia |
|---|---|---|---|
| `react-native-iap`, RevenueCat | StoreKit 2 (`Product`, `Transaction`) | Capacidad In-App Purchase | StoreKit 2 es `async/await` puro: `Product.products(for:)`, `product.purchase()`, `Transaction.currentEntitlements`. La verificación de recibos viene firmada y validada en el dispositivo. RevenueCat sigue teniendo sentido para analytics y backend de suscripciones, pero la API base ya no duele. |
| `@stripe/stripe-react-native` | Stripe iOS SDK (SPM) | Ninguno | Mismo SDK oficial. Recordar: los bienes digitales consumidos en la app pasan por In-App Purchase, no por Stripe; App Review lo revisa. |
| Apple Pay | PassKit (`PKPaymentAuthorizationController`) | Capacidad Apple Pay con Merchant ID | Para bienes físicos y servicios. El botón nativo (`PayWithApplePayButton`) es SwiftUI. |
| `expo-store-review` | StoreKit (`requestReview` en el entorno) | Ninguno | `@Environment(\.requestReview)` y una llamada. El sistema decide si muestra el diálogo. |

## Notificaciones

| Expo | Apple | Permiso / capacidad | Lo que cambia |
|---|---|---|---|
| `expo-notifications` (push) | UserNotifications + APNs | Capacidad Push Notifications; `registerForRemoteNotifications` | Expo tenía su servicio de push que te ahorraba hablar con APNs; ahora tu backend habla con APNs directo (token de auth con la clave `.p8`) o vía Firebase Cloud Messaging. El token del dispositivo llega a `AppDelegate`, que en SwiftUI se agrega con `@UIApplicationDelegateAdaptor`. |
| `expo-notifications` (locales) | UserNotifications (`UNUserNotificationCenter`) | Solicitud de permiso en tiempo de ejecución, sin clave en plist | `requestAuthorization(options:)` es `async`. Las locales se programan con triggers de tiempo, calendario o ubicación. |
| Notificaciones ricas (imagen, acciones) | Notification Service Extension y Content Extension | Target de extensión | Un target aparte, en Swift, que modifica la notificación antes de mostrarla. Es el primer sitio donde muchos equipos escriben Swift aunque la app sea React Native. |
| Badges | `UNUserNotificationCenter.setBadgeCount` | Incluido en el permiso de notificaciones | Directo. |

## Dispositivo, archivos y sistema

| Expo | Apple | Permiso / capacidad | Lo que cambia |
|---|---|---|---|
| `expo-file-system` | Foundation (`FileManager`, `URL`) | Ninguno | `Documents` con backup, `Caches` sin backup y borrable. Ver el post de persistencia. |
| `expo-document-picker` | `.fileImporter` (SwiftUI) | Ninguno | Un modifier de SwiftUI que abre el selector de archivos del sistema y devuelve URLs con acceso de seguridad (`startAccessingSecurityScopedResource`). |
| `expo-sharing` | `ShareLink` (SwiftUI) o `UIActivityViewController` | Ninguno | `ShareLink(item:)` es una línea. Para compartir con vista previa personalizada, `Transferable`. |
| `expo-clipboard` | `UIPasteboard` | Ninguno; iOS avisa al usuario cuando lees el portapapeles | Leer el portapapeles sin acción del usuario muestra un aviso del sistema. Solo se lee tras un toque explícito. |
| `expo-haptics` | `.sensoryFeedback` (SwiftUI) o `UIImpactFeedbackGenerator` | Ninguno | `.sensoryFeedback(.success, trigger: value)` como modifier. |
| `expo-device`, `expo-constants` | `UIDevice`, `ProcessInfo`, `Bundle.main` | Ninguno | El nombre del modelo exacto ("iPhone 16 Pro") no está expuesto; `utsname` da el identificador de hardware y hay que mapearlo. |
| `expo-network` | Network (`NWPathMonitor`) | Ninguno | Monitor de conectividad como `AsyncStream`. Reporta si hay ruta, no si internet funciona. |
| `expo-linking` | `.onOpenURL`, `openURL` del entorno | `LSApplicationQueriesSchemes` para consultar si otra app está instalada | Cubierto en el [post de navegación](/es/blog/navegacion-en-swiftui-sin-file-based-routing). |
| `expo-web-browser` | `SFSafariViewController` (envuelto) | Ninguno | Safari dentro de la app, con cookies compartidas con Safari. Para contenido propio, `WKWebView`. |
| `expo-localization` | Foundation (`Locale`, `String(localized:)`, String Catalogs) | Ninguno | Los String Catalogs (`.xcstrings`) reemplazan a los archivos JSON de i18n, con plurales y variantes por dispositivo integrados. Xcode extrae las cadenas automáticamente. |
| `expo-calendar`, `expo-contacts` | EventKit, Contacts | `NSCalendarsFullAccessUsageDescription`, `NSContactsUsageDescription` | Desde iOS 17, el calendario tiene acceso de "solo escribir" sin permiso completo (`EKEventEditViewController`). Contactos permite acceso limitado. |
| `expo-sensors` | CoreMotion | `NSMotionUsageDescription` | Acelerómetro, giroscopio, podómetro. Los datos de salud van aparte, en HealthKit, con su propio permiso por tipo de dato. |
| `expo-background-fetch`, `expo-task-manager` | BackgroundTasks (`BGAppRefreshTask`, `BGProcessingTask`) | Capacidad Background Modes con Background fetch / processing; identificadores en `Info.plist` | El sistema decide cuándo corre la tarea según el uso de la app; no hay garantía de frecuencia. Es el punto donde más expectativas se rompen. |
| `expo-updates` | No existe | — | No hay actualizaciones OTA de código en nativo. Configuración remota y feature flags, sí; código, no. |

## Permisos: Info.plist y el crash silencioso

Cada permiso en iOS tiene dos mitades. La primera es una **clave en `Info.plist`** cuyo valor es el texto que el usuario lee en el diálogo del sistema ("Esta app usa la cámara para escanear tus recibos"). La segunda es la **llamada en tiempo de ejecución** que dispara el diálogo. Expo unía las dos: el config plugin del paquete escribía la clave por ti, con un texto por defecto, y la función `requestPermissionsAsync` hacía la llamada.

En nativo, si haces la llamada sin haber escrito la clave, la app **se cierra en ese instante**. No hay excepción de Swift que atrapar, no hay diálogo, no hay error en la consola de la app. El mensaje aparece en el log del sistema (en Console.app, o en la salida de Xcode si estás conectado), y dice algo como que la app se terminó por un motivo de privacidad porque intentó acceder a datos sensibles sin una descripción de uso. Es el crash silencioso de la primera semana, y también el que aparece en producción cuando alguien agrega Face ID a una app que solo tenía Touch ID.

Las claves más comunes, para tenerlas a mano:

| Capacidad | Clave de `Info.plist` |
|---|---|
| Cámara | `NSCameraUsageDescription` |
| Micrófono | `NSMicrophoneUsageDescription` |
| Fototeca (leer) | `NSPhotoLibraryUsageDescription` |
| Fototeca (solo guardar) | `NSPhotoLibraryAddUsageDescription` |
| Ubicación al usar | `NSLocationWhenInUseUsageDescription` |
| Ubicación siempre | `NSLocationAlwaysAndWhenInUseUsageDescription` |
| Face ID | `NSFaceIDUsageDescription` |
| Contactos | `NSContactsUsageDescription` |
| Calendario | `NSCalendarsFullAccessUsageDescription` |
| Movimiento | `NSMotionUsageDescription` |
| Bluetooth | `NSBluetoothAlwaysUsageDescription` |
| Seguimiento entre apps (ATT) | `NSUserTrackingUsageDescription` |
| Red local | `NSLocalNetworkUsageDescription` |
| Salud (leer / escribir) | `NSHealthShareUsageDescription` / `NSHealthUpdateUsageDescription` |

Tres reglas sobre el texto:

1. **Explica el beneficio concreto**, no la capacidad. "Para escanear recibos" en vez de "Para usar la cámara". App Review rechaza descripciones vagas, y la tasa de aceptación del usuario cambia con el texto.
2. **Localízalo.** Las claves de `Info.plist` se traducen en `InfoPlist.xcstrings`; una app en español con el diálogo en inglés se nota.
3. **Pide el permiso en contexto**, cuando el usuario toca la función que lo necesita, no al arrancar. Cada permiso se pide una sola vez; si el usuario dice que no, solo puede cambiarlo en Ajustes, y tu app tiene que ofrecer un botón que abra `UIApplication.openSettingsURLString`.

Un detalle que en Expo no se veía: desde iOS 17, las apps que usan ciertas APIs consideradas sensibles (`UserDefaults`, marcas de tiempo de archivos, espacio en disco, tiempo de arranque del sistema) deben declarar el motivo en un **Privacy Manifest** (`PrivacyInfo.xcprivacy`), y lo mismo aplica a los SDK de terceros que incluyas. Sin él, App Store Connect rechaza el envío con un correo de advertencia. El [post de build y App Store](/es/blog/build-firma-y-app-store-lo-que-eas-te-escondia) lo cubre.

## Un patrón para pedir permisos sin repetir código

Cada framework pide permiso a su manera: CoreLocation con un delegate, AVFoundation con `async`, UserNotifications con `async throws`, Photos con un callback. Lo que hago es normalizarlos detrás de un solo tipo, para que las vistas no sepan de frameworks:

```swift
enum PermissionStatus {
    case notDetermined, granted, denied, restricted
}

protocol Permission: Sendable {
    var status: PermissionStatus { get async }
    func request() async -> PermissionStatus
}

struct CameraPermission: Permission {
    var status: PermissionStatus {
        get async {
            switch AVCaptureDevice.authorizationStatus(for: .video) {
            case .authorized: .granted
            case .denied: .denied
            case .restricted: .restricted
            case .notDetermined: .notDetermined
            @unknown default: .denied
            }
        }
    }

    func request() async -> PermissionStatus {
        let granted = await AVCaptureDevice.requestAccess(for: .video)
        return granted ? .granted : .denied
    }
}
```

Y una vista que lo usa de la misma forma para cualquier permiso:

```swift
struct PermissionGate<Content: View>: View {
    let permission: any Permission
    let rationale: String
    @ViewBuilder let content: () -> Content
    @State private var status: PermissionStatus = .notDetermined
    @Environment(\.openURL) private var openURL

    var body: some View {
        Group {
            switch status {
            case .granted:
                content()
            case .notDetermined:
                ContentUnavailableView(rationale, systemImage: "lock", description: Text("Necesitamos tu permiso para continuar."))
                    .overlay(alignment: .bottom) {
                        Button("Permitir") { Task { status = await permission.request() } }
                    }
            case .denied, .restricted:
                ContentUnavailableView("Permiso desactivado", systemImage: "gear")
                    .overlay(alignment: .bottom) {
                        Button("Abrir Ajustes") {
                            openURL(URL(string: UIApplication.openSettingsURLString)!)
                        }
                    }
            }
        }
        .task { status = await permission.status }
    }
}
```

Con eso, `PermissionGate(permission: CameraPermission(), rationale: "Escanea tus recibos") { ScannerView() }` resuelve la pantalla de explicación, la solicitud y el caso denegado, y agregar un permiso nuevo es implementar el protocolo una vez.

## Lo que Apple no cubre, y de dónde sale

Hay dominios donde no hay framework de Apple y la respuesta es un SDK de terceros por Swift Package Manager:

- **Analytics y crash reporting**: Firebase (Analytics, Crashlytics), Sentry, PostHog, Mixpanel. Todos con SPM. MetricKit de Apple da diagnósticos agregados sin SDK, pero no reemplaza a un crash reporter.
- **Mapas de Google, Mapbox**: SDK oficiales por SPM. MapKit cubre la mayoría de los casos sin ellos.
- **Chat y video en tiempo real**: Stream, Sendbird, Twilio, Agora, LiveKit. Con SPM.
- **Feature flags y configuración remota**: Firebase Remote Config, LaunchDarkly, o un endpoint propio. Es el reemplazo parcial de OTA: lo que se puede cambiar sin release es lo que la app lee del servidor.
- **Backend as a service**: Firebase, Supabase (`supabase-swift`), Appwrite. Cubierto en el post de data.

La señal que uso para evaluar un SDK: si no está en SPM (solo CocoaPods o un `.xcframework` manual), probablemente no está mantenido para Swift moderno, y con Swift 6 estricto eso se vuelve un problema en la primera compilación. CocoaPods sigue funcionando, pero está en modo de mantenimiento desde 2024 y ya no es la ruta por defecto.

> La primera app que migré de Expo a nativo tenía catorce paquetes `expo-*`. Al terminar, doce eran frameworks de Apple sin dependencia, y dos eran SDK de terceros por SPM. El código de permisos, que en Expo eran catorce llamadas con la misma forma, terminó siendo un protocolo y catorce implementaciones de veinte líneas. Más código, y por primera vez sabía exactamente qué pedía cada uno.

## Preguntas frecuentes

### ¿Cómo sé qué framework usar para algo que no está en la tabla?

La documentación de Apple está organizada por framework, y el buscador de Xcode (la documentación integrada) es más rápido que la web. Mi atajo es buscar el nombre del paquete de Expo en su propio repositorio: el código fuente del módulo iOS de `expo-lo-que-sea` importa exactamente el framework de Apple que necesitas y muestra cómo lo usa.

### ¿Vale la pena usar UIKit para la cámara o hay algo en SwiftUI?

Para vista previa y captura, la sesión de AVFoundation se muestra con una capa de UIKit envuelta en `UIViewRepresentable`. No hay una vista de cámara en SwiftUI puro. Es una de las pocas envolturas de UIKit que toda app con cámara escribe, y se escribe una vez.

### ¿Qué pasa con los permisos en el simulador?

La mayoría se pueden probar: el simulador muestra los diálogos y guarda la decisión. Cámara y ciertos sensores no funcionan en el simulador; para eso hace falta un dispositivo. `xcrun simctl privacy` permite conceder o revocar permisos por línea de comandos, útil en tests de UI.

### ¿Cómo reseteo un permiso ya concedido para volver a probar el diálogo?

En el dispositivo, desinstalando la app (el permiso se borra, el Keychain no). En el simulador, `xcrun simctl privacy booted reset all com.miapp.bundle`. En Ajustes, Privacidad, se puede cambiar el estado, pero no volver a "no determinado".

### ¿Los SDK de terceros también necesitan claves de permiso?

Sí. Si un SDK de analytics accede a la red local, al IDFA o al portapapeles, tu `Info.plist` necesita la clave correspondiente y tu Privacy Manifest debe declararlo, aunque el acceso lo haga el SDK. App Review considera responsable a la app, no al SDK.

## Conclusión

Cada paquete `expo-*` era un envoltorio con forma uniforme sobre un framework de Apple con forma propia. Pasar a nativo es quitar el envoltorio: AVFoundation para cámara y audio, CoreLocation y MapKit para ubicación, AuthenticationServices para login, StoreKit 2 para pagos, UserNotifications con APNs para push, y Foundation para archivos y localización. Lo que se gana es control y APIs del día uno; lo que se pierde es la uniformidad, y eso se recupera con un protocolo propio de permisos y con SPM para lo que Apple no cubre. El crash silencioso por una clave de `Info.plist` que falta es el bug que todos cometen una vez y ninguno dos.

Para empezar: haz tu propia tabla con los paquetes de tu `package.json`, escribe las claves de `Info.plist` antes que el código que las necesita, normaliza los permisos detrás de un protocolo, y elige SPM como único gestor de dependencias. El [siguiente post](/es/blog/ui-theming-y-animaciones-en-swiftui-sin-nativewind-ni-reanimated) cubre la capa visual: design tokens sin NativeWind, animaciones sin Reanimated, y Dynamic Type y accesibilidad como estándar y no como extra.
