Skip to Content

SDK de iOS: sobrevivir a la suspensión

El marco CoreLocation de Apple proporciona herramientas poderosas para el seguimiento en segundo plano, pero iOS es extremadamente agresivo a la hora de suspender aplicaciones para ahorrar memoria y duración de la batería.

Esta página explica exactamente cómo interactúa Tracelet con las estrictas políticas de ejecución en segundo plano de Apple y cómo puede configurarlo para su escenario específico.


Configuración de permisos e información.plist

Apple aplica estrictos requisitos de privacidad. Para utilizar Tracelet de manera efectiva, debe declarar exactamente por qué necesita ciertos permisos en su ios/Runner/Info.plist. Apple rechazará su aplicación durante la revisión de la App Store si faltan estas cadenas o no explican claramente el caso de uso.

1. Descripciones de uso requeridas

Agregue las siguientes claves a su Info.plist:

<!-- Required for basic tracking --> <key>NSLocationWhenInUseUsageDescription</key> <string>We need your location to track your route while the app is open.</string> <!-- Required for background tracking when the app is minimized or killed --> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>We need background location to record your route even when the app is closed.</string> <!-- Required for the smart motion-detection battery saving engine --> <key>NSMotionUsageDescription</key> <string>Motion detection allows battery-efficient tracking by pausing GPS when stationary.</string>

2. Modos de fondo

Para que Tracelet se ejecute en segundo plano (y ejecute código Dart sin cabeza cuando se cierre la aplicación), debe declarar las capacidades adecuadas de los modos de fondo en Xcode.

Alternativamente, agréguelos directamente a su Info.plist:

<key>UIBackgroundModes</key> <array> <string>location</string> <string>fetch</string> <!-- Only add 'audio' if you are using 'preventSuspend: true' (See Scenario 1 below) --> </array>

3. Eliminación de funciones opcionales (sin fallos)

Si su cliente no quiere funciones específicas (por ejemplo, no quiere rastrear movimientos o tipos de actividad), puede omitir con seguridad la clave correspondiente de su Info.plist. El código Swift nativo de Tracelet comprueba de forma segura la presencia de claves antes de solicitar funciones.

Clave de funciónEfecto cuando se elimina de Info.plist
NSMotionUsageDescriptionSeguro. iOS volverá silenciosamente a comprobar los cambios de ubicación en busca de movimiento. CMMotionActivityManager no se invocará y no se solicitarán permisos de movimiento al usuario.
UIBackgroundModes -> audioSeguro. Solo es necesario si su configuración establece preventSuspend: true para evitar la suspensión del aislamiento de Dart.
NSLocationAlwaysAndWhenInUseUsageDescriptionSeguro. La aplicación solo realizará un seguimiento mientras esté en primer plano (o temporalmente a través de la pastilla azul). Tracelet maneja la elegante degradación internamente.

Escenario 1: La aplicación Fitness (alta precisión, sin suspensión)

Conceptos explorados: Tipos de actividad, suspensión, activación por audio

El problema

Tu usuario está corriendo un maratón. Abren su aplicación, inician la carrera y bloquean su iPhone. Cinco minutos más tarde, iOS decide que necesita RAM para ejecutar una copia de seguridad de iCloud en segundo plano, por lo que suspende por completo el aislamiento de Dart de su aplicación. El código rápido nativo de Tracelet continúa recopilando puntos de GPS, pero debido a que el aislamiento de Dart está congelado, la interfaz de usuario de Flutter deja de actualizar el contador de distancia y se eliminan todos los sockets web de seguimiento en vivo que esté ejecutando en Dart.

Cómo lo resuelve Tracelet: evitar la suspensión

Para mantener el aislado Flutter Dart ejecutándose indefinidamente mientras la pantalla está apagada, debes engañar a iOS haciéndole creer que la aplicación está reproduciendo medios activamente.

ios: tl.IosConfig( activityType: tl.LocationActivityType.fitness, // Optimizes GPS filtering for running preventSuspend: true, )

Configurar preventSuspend: true hace que Tracelet reproduzca un clip de audio silencioso e imperceptible en un bucle. Debido a que iOS cree que el usuario está escuchando música, NUNCA suspenderá el aislamiento de Dart.

Nota de revisión de la App Store: El uso de preventSuspend requiere la capacidad del modo en segundo plano audio en Xcode. Apple rechazará su aplicación si utiliza audio de fondo únicamente para mantener viva la aplicación sin un motivo legítimo de cara al usuario (por ejemplo, un rastreador de actividad física que reproduce señales de voz o una aplicación de navegación que indica instrucciones paso a paso). ¡No utilice esto para el seguimiento silencioso de flotas!

Para habilitar esto: Abra su proyecto de iOS en Xcode, vaya a Firma y capacidades -> + Capacidad -> Modos de fondo -> marque Audio, AirPlay e imagen en imagen.


Escenario 2: El radar social (baja potencia, burdo)

Conceptos explorados: Cambios significativos, niveles de autorización

El problema

Estás creando una aplicación de red social que notifica al usuario cuando un amigo está cerca. No necesita precisión paso a paso; sólo necesita saber aproximadamente en qué vecindario se encuentran. Tampoco desea mostrar el aterrador mensaje de permiso “Permitir siempre”, ya que los usuarios lo rechazarán.

Cómo lo resuelve Tracelet: cambios significativos

En lugar de encender el chip GPS, Tracelet puede depender completamente de las transferencias de las torres de telefonía móvil.

ios: tl.IosConfig( useSignificantChangesOnly: true, locationAuthorizationRequest: tl.LocationAuthorizationRequest.whenInUse, disableLocationAuthorizationAlert: true, )
  1. Solo cambios significativos: Al habilitar esto, Tracelet le dice a iOS que solo active la aplicación cuando el dispositivo salte a una torre celular completamente diferente (generalmente de 500 ma varios kilómetros). El consumo de batería es prácticamente nulo.
  2. Autorización cuando esté en uso: Solo solicita el permiso “Cuando esté en uso”. Tracelet respeta esto y no activará el mensaje nativo de Apple que le indica al usuario que vaya a Configuración para habilitar “Siempre”. (Para saber cómo activar estas solicitudes de permiso desde Dart, consulte la página Flutter SDK: Permisos).

Escenario 3: El indicador de ubicación azul (showsBackgroundLocationIndicator)

Conceptos explorados: Indicador de ubicación en segundo plano

Lo que realmente hace esta bandera

showsBackgroundLocationIndicator se asigna directamente a Apple CLLocationManager.showsBackgroundLocationIndicator. El nombre es contrario a la intuición: es una opción para mostrar el indicador, no un interruptor para ocultarlo:

ValorSignificado
trueMuestre la pastilla azul de la barra de estado/indicador de isla dinámica mientras la aplicación utiliza la ubicación en segundo plano.
false (predeterminado)Solicitud para ocultar el indicador para uso de ubicación en segundo plano.

Error común: configurar showsBackgroundLocationIndicator: true no apaga* el indicador azul, sino que lo enciende. Si tu objetivo es ocultarlo, déjalo false (el valor predeterminado). Configurarlo como true solo es útil cuando quieres que la pastilla esté visible (por ejemplo, para satisfacer el seguimiento en segundo plano “Cuando está en uso”, a continuación).

Cuando quieres la píldora (seguimiento temporal en segundo plano en “Cuando está en uso”)

Solo tiene el permiso “Cuando está en uso”, pero desea realizar un seguimiento mientras el usuario completa una tarea (por ejemplo, recoger un viaje compartido). iOS permite esto solo si el indicador es visible, para que el usuario sepa que se está realizando el seguimiento:

ios: tl.IosConfig( showsBackgroundLocationIndicator: true, )

Para habilitar esto: el modo en segundo plano location debe estar activado en Xcode (Firma y capacidades → Modos en segundo plano → Actualizaciones de ubicación).

Por qué false no siempre lo oculta

Incluso con showsBackgroundLocationIndicator: false, iOS fuerza el indicador a activarse en estos casos; la bandera no puede anularlos:

  1. Autorización “Cuando esté en uso”: la ubicación en segundo plano siempre muestra el indicador. Sólo la autorización completa “Siempre” puede suprimirlo.
  2. useBackgroundActivitySession: true (iOS 17+): Apple requiere el indicador mientras CLBackgroundActivitySession está activo (Escenario 4).
  3. Actividades en vivo activas (Escenario 5).
  4. Cualquier sesión continua de ubicación en segundo plano: si la aplicación ejecuta startUpdatingLocation sin parar, el indicador permanece encendido independientemente de la bandera.

Para mantener el indicador oculto

  1. Obtenga la autorización completa “Siempre” (tenga en cuenta que iOS puede otorgar primero un “Cuando esté en uso” provisional hasta que el usuario confirme más tarde “Cambiar a Siempre”; la píldora aparece hasta entonces).
  2. Mantenga showsBackgroundLocationIndicator: false (no lo configure en true).
  3. Evite la ubicación de fondo continua innecesaria. El modo solo de geocerca utiliza monitoreo de región nativa (sin GPS continuo), por lo que no muestra ningún indicadorstartGeofences() no ejecuta actualizaciones continuas en el modo estándar.

Escenario 4: El indicador de isla dinámica (iOS 17+)

Conceptos explorados: Sesión de actividad previa

El problema

Quiere los beneficios del indicador de píldora azul, pero quiere una integración nativa más moderna con Dynamic Island en los iPhones modernos y quiere reducir las posibilidades de que el sistema operativo interrumpa su sesión en segundo plano.

Cómo lo resuelve Tracelet

Tracelet se integra con CLBackgroundActivitySession de Apple (introducido en iOS 17). Esto proporciona un indicador destacado de Dynamic Island y crea una sesión formal con el sistema operativo, indicándole que no suspenda su aplicación.

ios: tl.IosConfig( useBackgroundActivitySession: true, )

Requisitos de revisión de la App Store: Apple requiere explícitamente una explicación clara de por qué su aplicación necesita una ubicación en segundo plano persistente. Si usa CLBackgroundActivitySession, debe proporcionar una justificación en tres lugares o su aplicación será rechazada:

  1. Notas de revisión de App Store Connect: Debe proporcionar una explicación clara por escrito al revisor sobre por qué la aplicación necesita esta función, junto con un enlace a un video de demostración que muestra la función en acción.
  2. Descripción de la App Store: La descripción de tu aplicación pública debe indicar claramente que la aplicación usa la ubicación en segundo plano (por ejemplo, “Esta aplicación usa la ubicación en segundo plano para rastrear tus carreras incluso cuando la aplicación está cerrada”).
  3. Incorporación en la aplicación: Antes de solicitar permisos de ubicación, la interfaz de usuario de su aplicación debe explicar claramente al usuario por qué se necesita la ubicación en segundo plano.

Escenario 5: actividades en vivo (pantalla de bloqueo y interfaz de usuario dinámica de la isla)

Conceptos explorados: ActivityKit, widgets de pantalla de bloqueo, isla dinámica

El problema

Mientras realiza el seguimiento en segundo plano en iOS 17+, desea un indicador rico y visible en la pantalla de bloqueo y en la isla dinámica, para que el usuario siempre sepa que el seguimiento está activo, en lugar de solo la pequeña pastilla de ubicación azul.

Cómo lo resuelve Tracelet

Tracelet se integra con ActivityKit de Apple. Si proporciona liveActivityConfig y agrega una extensión de widget, Tracelet inicia automáticamente una actividad en vivo cuando comienza el seguimiento y la finaliza cuando se detiene.

La actividad en vivo es una capa de interfaz de usuario encima del canal de fondo estándar de Tracelet. La eficiencia de la batería en sí proviene del motor de detección de movimiento que pausa el GPS cuando está parado y de la integración de la sesión en segundo plano (Escenario 4), no del widget de actividad. Tracelet no abre una segunda secuencia CLLocationUpdate.liveUpdates(), lo que duplicaría el trabajo del GPS.

ios: tl.IosConfig( liveActivityConfig: tl.LiveActivityConfig( title: 'Ride in progress', body: 'Tracking your route to the destination...', ), )

Actualizar la actividad en vivo durante el seguimiento (updateNotification())

Para cambiar lo que muestra la actividad en vivo después de que haya comenzado el seguimiento, actualice liveActivityConfig a través de setConfig() y luego llame a Tracelet.updateNotification(). Desde v3.6.8, esto actualiza la actividad en vivo desde la configuración más reciente sin reiniciar el proceso de seguimiento: la contraparte multiplataforma de actualizar la notificación de servicio en primer plano de Android:

await Tracelet.setConfig( const tl.Config( ios: tl.IosConfig( liveActivityConfig: tl.LiveActivityConfig( title: 'Ride in progress', body: 'Arriving in 2 minutes', ), ), ), ); await Tracelet.updateNotification();
ℹ️

Solo el cuerpo se actualiza en una actividad en ejecución; title vive en el ActivityAttributes inmutable y no puede cambiar sin finalizar y volver a solicitar la actividad (una restricción de ActivityKit).

La actividad en vivo está vinculada al subestado en movimiento (se descarta cuando Tracelet pausa el GPS cuando se queda estacionario), por lo que es posible que no esté en la pantalla en el momento en que la actualiza. Mientras una sesión de seguimiento está activa, updateNotification() maneja esto por usted: actualiza la actividad en ejecución en su lugar o la representa con el contenido más reciente si se descartó. Es una operación no operativa segura cuando no se establece liveActivityConfig o se detiene el seguimiento.

Configuración de la extensión del widget Xcode (obligatorio)

¿Para qué es esto? Esta configuración permite que su aplicación muestre una actividad en vivo en la pantalla de bloqueo de iOS y en la isla dinámica mientras el seguimiento está activo. Proporciona a los usuarios información visible sobre su sesión en curso sin abrir la aplicación.

¿Ahorra batería? No. La actividad en vivo es puramente una capa de interfaz de usuario y no ahorra batería. La eficiencia de la batería proviene enteramente del motor central en segundo plano de Tracelet (detección de movimiento, pausa del GPS cuando está parado y sesiones en segundo plano).

¿Es obligatorio? No. Esta configuración es completamente opcional. Si omite esto, Tracelet seguirá rastreando perfectamente en segundo plano, pero los usuarios solo verán indicadores estándar del sistema (como la píldora de ubicación azul) en lugar de su interfaz de usuario personalizada.

A diferencia de Android, los complementos de Flutter no pueden crear widgets de iOS de forma dinámica. Para habilitar Live Activity, debe agregar un destino Extensión de widget a su aplicación iOS en Xcode:

  1. Abra ios/Runner.xcworkspace en Xcode.
  2. Vaya a Archivo -> Nuevo -> Destino… y seleccione Extensión de widget.
  3. Nómbrelo TraceletWidget. Asegúrate de que Incluir actividad en vivo esté marcado.
  4. TANTO EN Info.plist de su aplicación como en Info.plist de su nueva extensión de widget (haga clic derecho en Xcode -> Abrir como -> Código fuente), debe agregar la siguiente clave dentro del <dict> principal:
<key>NSSupportsLiveActivities</key> <true/>
  1. Sincronice la versión de la extensión (solo importa para el envío de la App Store): Apple rechaza una extensión de aplicación cuyo CFBundleShortVersionString / CFBundleVersion no coincida con la aplicación host. Configure la Versión y la Compilación del destino del widget para que coincidan con su aplicación: seleccione el destino TraceletWidgetGeneralIdentidad, y establezca Versión en la versión de su aplicación y Compilación en el número de compilación de su aplicación (los mismos valores que su pubspec.yaml).

No intente configurarlos pegando $(FLUTTER_BUILD_NAME) / $(FLUTTER_BUILD_NUMBER) en Info.plist del widget. Esas variables solo están definidas para el objetivo Runner (a través de Generated.xcconfig de Flutter); en el destino del widget, resuelven estar vacíos, por lo que las versiones silenciosamente no coincidirán. Utilice la configuración de compilación del objetivo (MARKETING_VERSION / CURRENT_PROJECT_VERSION) como se indica arriba. Este es solo un requisito de envío: una discrepancia no bloquea la aplicación en tiempo de ejecución.

  1. NO vincule las dependencias de Flutter: No vincule FlutterGeneratedPluginSwiftPackage ni el motor de Flutter a su extensión de widget. Hacerlo provoca un bloqueo de dyld (Library not loaded) en el momento del lanzamiento en el modo de lanzamiento, porque Flutter no integra sus marcos dinámicos de SPM en las extensiones de aplicación.
  2. Reemplace el contenido TraceletWidgetLiveActivity.swift generado automáticamente por lo siguiente. Tenga en cuenta que definimos manualmente la estructura TraceletActivityAttributes aquí en lugar de importar el SDK: ActivityKit coincide con la actividad por el nombre (no calificado) y la forma de la estructura, por lo que esto mantiene su widget liviano y evita vincular el SDK a la extensión:
import ActivityKit import WidgetKit import SwiftUI // Define the exact struct expected by Tracelet's native core public struct TraceletActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { public var status: String public init(status: String) { self.status = status } } public var title: String public init(title: String) { self.title = title } } @main struct TraceletWidgetBundle: WidgetBundle { var body: some Widget { TraceletWidgetLiveActivity() } } struct TraceletWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: TraceletActivityAttributes.self) { context in // Lock Screen / banner UI — fully self-contained (no SDK import needed) HStack(spacing: 12) { Image(systemName: "location.fill").foregroundColor(.blue) VStack(alignment: .leading) { Text(context.attributes.title).font(.headline) Text(context.state.status).font(.subheadline).foregroundColor(.secondary) } Spacer() } .padding() } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Text("Tracking") } DynamicIslandExpandedRegion(.trailing) { Text("Live") } DynamicIslandExpandedRegion(.bottom) { Text(context.state.status) } } compactLeading: { Image(systemName: "location.fill").foregroundColor(.blue) } compactTrailing: { Text("Live") } minimal: { Image(systemName: "location.fill").foregroundColor(.blue) } } } }

Esta vista de SwiftUI es totalmente tuya para personalizarla: dale el estilo que quieras dentro del bloque ActivityConfiguration, asignando los datos de context.attributes.title y context.state.status.

Establezca el objetivo de implementación de la extensión de widget en iOS 16.2. La plantilla “Extensión de widget” de Xcode a menudo agrega widgets de control y una anotación @available(iOS 18.0, *) en @main WidgetBundle. Si esa anotación es mayor que el objetivo de implementación de la extensión, el paquete de widgets (y por lo tanto su actividad en vivo) falla silenciosamente al registrarse (el sistema registra Activity had no descriptor) y la extensión puede fallar en versiones anteriores del sistema operativo. Mantenga el paquete @main disponible en el piso de implementación de la extensión y empaquete cualquier widget solo para iOS-18 (por ejemplo, widgets de control) en if #available(iOS 18.0, *).


Escenario 6: El estado terminado (App Force-Quit)

Conceptos explorados: Ciclo de vida de la aplicación, cierre forzado versus suspensión del sistema operativo

El problema

Su usuario desliza hacia arriba desde el selector de aplicaciones y fuerza el cierre completo de la aplicación. Espera que Tracelet continúe rastreándolos en segundo plano, pero las ubicaciones dejan de aparecer.

Cómo lo maneja Apple

A diferencia de Android, donde un servicio en primer plano a menudo puede sobrevivir al deslizar el dedo para descartar, iOS aplica estrictamente la intención del usuario.

Según la Documentación oficial de CoreLocation de Apple :

“Si el usuario fuerza el cierre de su aplicación, el sistema no la inicia automáticamente cuando llegan nuevos eventos de ubicación. El usuario debe reiniciar explícitamente su aplicación antes de que el sistema reanude la entrega de eventos de ubicación.”

Cuando un usuario fuerza el cierre manual de la aplicación:

  1. Actualizaciones de ubicación estándar: Se detuvo por completo.
  2. Cambios de ubicación significativos (SLC): Se detuvo por completo.
  3. Monitoreo de región (geocercas): Se detuvo por completo.

¿Funcionará en el estado terminado?

  • Si el sistema operativo finalizó la aplicación (por ejemplo, debido a la presión de la memoria en segundo plano): Sí. Apple reiniciará automáticamente su aplicación en segundo plano cuando ocurra un evento de ubicación (como un cambio significativo o un activador de geocerca). El núcleo Rust nativo de Tracelet se activará y lo manejará sin problemas.
  • Si el usuario fuerza explícitamente el cierre de la aplicación: No. La aplicación permanecerá inactiva hasta que el usuario toque manualmente el ícono de la aplicación para abrirla nuevamente.

Cómo lo resuelve Tracelet

Tracelet no puede eludir las limitaciones fundamentales del sistema operativo de Apple. Sin embargo, Tracelet asegura que:

  1. Todas las ubicaciones no sincronizadas se conservan de forma segura en la base de datos Rust SQLite antes de cerrar la aplicación.
  2. Tan pronto como el usuario vuelve a abrir la aplicación, Tracelet reanuda instantáneamente el seguimiento y sincroniza todos los datos fuera de línea sin perder el ritmo.