tutoriales.com

Maestría en Background Tasks: Ejecución de Tareas en Segundo Plano en iOS con Swift

Este tutorial exhaustivo te guiará a través del complejo mundo de las tareas en segundo plano en iOS, permitiéndote mantener tus aplicaciones actualizadas y reactivas incluso cuando no están en primer plano. Exploraremos desde las técnicas tradicionales hasta los APIs más modernos como BackgroundTasks framework y Push Notifications con Content-Available.

Intermedio20 min de lectura10 views
Reportar error

🚀 Introducción a las Tareas en Segundo Plano en iOS

En el ecosistema móvil, la capacidad de una aplicación para realizar tareas en segundo plano es crucial para ofrecer una experiencia de usuario fluida y actualizada. Imagina una aplicación de noticias que siempre muestra los titulares más recientes al abrirse, o una aplicación de fitness que sincroniza tus datos de entrenamiento sin que tengas que iniciarla manualmente. Todo esto es posible gracias a las Background Tasks o tareas en segundo plano.

El sistema operativo iOS es conocido por su estricta gestión de recursos y batería, lo que presenta un desafío para los desarrolladores que buscan ejecutar código cuando su aplicación no está activamente en primer plano. Sin embargo, Apple ha evolucionado sus APIs para proporcionar formas controladas y eficientes de lograrlo, equilibrando la funcionalidad con la duración de la batería del dispositivo. Comprender y aplicar correctamente estas técnicas es fundamental para cualquier desarrollador iOS que aspire a crear aplicaciones robustas y de alto rendimiento.

Este tutorial te sumergirá profundamente en las diversas estrategias y frameworks que iOS ofrece para la ejecución de tareas en segundo plano. Desde la gestión de la vida útil de una aplicación hasta la implementación de BackgroundTasks y el uso inteligente de las notificaciones push, cubriremos cada aspecto con ejemplos prácticos y las mejores prácticas. Prepárate para llevar tus aplicaciones iOS al siguiente nivel, asegurando que estén siempre listas y actualizadas para tus usuarios.

💡 Consejo: La clave para una buena implementación de tareas en segundo plano es la eficiencia. Ejecuta solo lo necesario y hazlo rápidamente para no agotar la batería del usuario.

📖 El Ciclo de Vida de la Aplicación y el Segundo Plano

Antes de sumergirnos en los detalles de las tareas en segundo plano, es esencial entender cómo iOS gestiona el ciclo de vida de una aplicación y cómo esto afecta la ejecución de código en background. Una aplicación en iOS puede estar en varios estados:

  • Not Running: La aplicación no se ha lanzado o fue terminada por el sistema.
  • Inactive: La aplicación está en primer plano pero no está recibiendo eventos (por ejemplo, una llamada telefónica entrante).
  • Active: La aplicación está en primer plano y completamente operativa.
  • Background: La aplicación no está en primer plano, pero aún está ejecutando código. Puede estar haciendo una tarea de corta duración o esperando ser suspendida.
  • Suspended: La aplicación está en segundo plano y iOS la ha puesto en un estado en el que no puede ejecutar código. El sistema la puede Suspender en cualquier momento para liberar memoria.

Cuando una aplicación pasa de Active a Background, tiene un breve período de tiempo para finalizar cualquier tarea en curso o para iniciar una tarea de fondo de corta duración. Si no hay tareas pendientes y la aplicación no ha solicitado explícitamente más tiempo, iOS la suspenderá.

⏳ Solicitar Tiempo de Ejecución en Segundo Plano (Legacy)

Antes de iOS 7, la gestión del segundo plano era bastante limitada. Sin embargo, aún hoy, si necesitas un período muy corto y específico para finalizar una tarea crítica cuando la app va a segundo plano, puedes solicitar tiempo adicional. Esto es ideal para guardar el estado del usuario o completar una descarga pequeña.

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    var backgroundTask: UIBackgroundTaskIdentifier = .invalid

    func applicationDidEnterBackground(_ application: UIApplication) {
        backgroundTask = application.beginBackgroundTask {
            // Si se agota el tiempo antes de que terminemos, se llama a este bloque.
            application.endBackgroundTask(self.backgroundTask)
            self.backgroundTask = .invalid
        }

        // Inicia tu tarea de corta duración aquí
        DispatchQueue.global().async {
            print("Iniciando tarea de corta duración en segundo plano...")
            // Simula una tarea que toma 5 segundos
            Thread.sleep(forTimeInterval: 5)
            print("Tarea de corta duración finalizada.")

            // Es crucial llamar a endBackgroundTask cuando la tarea haya terminado
            if self.backgroundTask != .invalid {
                application.endBackgroundTask(self.backgroundTask)
                self.backgroundTask = .invalid
            }
        }
    }
}
⚠️ Advertencia: Este método (`beginBackgroundTask`) otorga un tiempo limitado (generalmente hasta 30 segundos). Si excedes este tiempo o no llamas a `endBackgroundTask`, tu aplicación será terminada por el sistema y podrías recibir una penalización por mal comportamiento. Úsalo con moderación.

✨ Background App Refresh: Actualización en Segundo Plano Inteligente

Background App Refresh (Actualización en Segundo Plano de la Aplicación) es una característica introducida en iOS 7 que permite a las aplicaciones actualizar su contenido en intervalos oportunos sin tener que ser lanzadas manualmente por el usuario. El sistema operativo es el que decide cuándo y con qué frecuencia permite que una aplicación se actualice, basándose en factores como los patrones de uso del usuario, la conectividad de red y el estado de la batería.

Esto es ideal para aplicaciones que necesitan cargar nuevos datos regularmente, como un cliente de correo electrónico o una aplicación de pronóstico del tiempo. El objetivo principal es que, cuando el usuario abra la aplicación, el contenido ya esté fresco y listo para ser mostrado, mejorando la percepción de rendimiento.

Configuración de Background App Refresh

Para habilitar Background App Refresh para tu aplicación, necesitas configurar algunas cosas:

  1. Habilitar la capacidad en Xcode: Ve a las Capabilities de tu proyecto y activa Background Modes. Marca la casilla Background fetch.
1. Navegador de Proyecto 2. Seleccionar Target 3. Signing & Capabilities 4. + Capability 5. Background Modes 6. Activar "Background Fetch"
  1. Configurar el minimumBackgroundFetchInterval: En tu AppDelegate, puedes sugerirle al sistema con qué frecuencia te gustaría que tu aplicación se actualizara. Sin embargo, ten en cuenta que esta es solo una sugerencia y iOS podría ignorarla si las condiciones no son favorables.
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Sugerir un intervalo de fetch. .minimum es la opción por defecto, que significa "nunca" o "cuando iOS decida".
// Otros valores son: .no, o un TimeInterval específico.
application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
// O para un intervalo más agresivo (pero aún solo una sugerencia):
// application.setMinimumBackgroundFetchInterval(60 * 60) // Cada hora

return true
}

// ... otros métodos del AppDelegate
}
  1. Implementar el método application(_:performFetchWithCompletionHandler:): Cuando iOS decida que es un buen momento para que tu aplicación se actualice en segundo plano, llamará a este método en tu AppDelegate.
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

// ... (otros métodos y propiedades)

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("Background fetch iniciado.")

// Aquí es donde realizarás tu tarea de actualización de contenido.
// Por ejemplo, descargar nuevos datos de un servidor.
fetchNewData { newDataAvailable in
if newDataAvailable {
print("Nuevos datos disponibles. Informando al sistema.")
completionHandler(.newData)
} else {
print("No hay nuevos datos. Informando al sistema.")
completionHandler(.noData)
}
}
}

private func fetchNewData(completion: @escaping (Bool) -> Void) {
// Simula una llamada a la red para obtener nuevos datos
DispatchQueue.global().asyncAfter(deadline: .now() + 3) { // Simula 3 segundos de red
let shouldReturnNewData = Bool.random() // Simula si hay o no nuevos datos
completion(shouldReturnNewData)
}
}
}
Es *crucial* llamar al `completionHandler` tan pronto como tu tarea haya terminado. Esto le informa a iOS que la tarea ha finalizado y permite que el sistema mida el consumo de energía y suspenda tu aplicación si es necesario. El `UIBackgroundFetchResult` que pasas (`.newData`, `.noData`, `.failed`) ayuda al sistema a aprender sobre la eficiencia de tu aplicación.
🔥 Importante: La tarea de `Background Fetch` debe ser *muy* rápida, idealmente no más de unos pocos segundos. Si tardas demasiado, iOS podría dejar de invocar tu `fetch` en el futuro.

☁️ Notificaciones Push con content-available: Despertando tu App Inteligentemente

Las notificaciones push son una herramienta poderosa para interactuar con los usuarios, pero también pueden ser utilizadas para iniciar tareas en segundo plano de manera inteligente, incluso cuando Background App Refresh no ha sido activado o cuando la aplicación está cerrada. Esto se logra mediante el uso del campo content-available en el payload de la notificación push.

Cuando iOS recibe una notificación push con "content-available": 1 y sin alerta sonora o visual (es decir, una silent push notification), la aplicación se despierta en segundo plano y se le da una cantidad de tiempo limitada (generalmente hasta 30 segundos) para descargar nuevo contenido. Si la aplicación ya está suspendida, esta es una forma de "despertarla" para realizar una actualización.

Configuración y Uso

  1. Habilitar Push Notifications: En las Capabilities de tu proyecto, activa Push Notifications.
1. Project Navigator 2. Seleccionar Target 3. Signing & Capabilities 4. + Capability 5. Push Notifications
  1. Registrarse para Notificaciones Remotas: En tu AppDelegate o en el arranque de tu aplicación, regístrate para recibir notificaciones remotas.
import UIKit
import UserNotifications

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Solicitar autorización para notificaciones (si aún no se ha hecho)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
print("Permiso para notificaciones: \(granted)")
guard granted else { return }
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
return true
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Device Token: \(token)")
// Envía este token a tu servidor para poder enviar notificaciones push a este dispositivo.
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Error al registrar para notificaciones remotas: \(error.localizedDescription)")
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("Notificación remota silenciosa recibida: \(userInfo)")

guard let aps = userInfo["aps"] as? [String: AnyObject] else { // [AnyHashable: Any] en realidad, pero 'AnyObject' es más común
completionHandler(.noData)
return
}

// Verificar si es una notificación con content-available
if aps["content-available"] as? Int == 1 {
// Realiza tu tarea de actualización de contenido aquí
fetchNewDataFromSilentPush { newDataAvailable in
if newDataAvailable {
print("Nuevos datos disponibles por push silencioso.")
completionHandler(.newData)
} else {
print("No hay nuevos datos por push silencioso.")
completionHandler(.noData)
}
}
} else {
// Es una notificación push regular (con alerta, sonido, etc.)
completionHandler(.noData)
}
}

private func fetchNewDataFromSilentPush(completion: @escaping (Bool) -> Void) {
// Simula la descarga de datos activada por una push silenciosa
DispatchQueue.global().asyncAfter(deadline: .now() + 5) { // Simula 5 segundos de red
let shouldReturnNewData = Bool.random()
completion(shouldReturnNewData)
}
}
}
  1. Enviar el payload de notificación: Tu servidor debe enviar un payload JSON similar a este:
{
"aps" : {
"content-available" : 1
},
"custom_data" : {
"article_id" : "12345"
}
}
Observa que no hay campos como `alert`, `sound` o `badge` dentro de `aps` para que sea una notificación *silenciosa*. Puedes incluir datos personalizados en el diccionario, como `custom_data`, que tu aplicación puede usar para saber qué tipo de contenido debe actualizar.
⚠️ Advertencia: El uso excesivo de `content-available` para despertar tu aplicación puede agotar la batería del usuario y hacer que iOS reduzca la frecuencia con la que tu aplicación recibe estas notificaciones silenciosas. Úsalas solo cuando sea realmente necesario.

⚙️ BackgroundTasks Framework: El Enfoque Moderno y Declarativo (iOS 13+)

Con la introducción de BackgroundTasks framework en iOS 13, Apple proporcionó un enfoque más estructurado y eficiente para programar tareas en segundo plano. Este framework permite a los desarrolladores registrar diferentes tipos de tareas (refresh o procesamiento) y especificar las condiciones bajo las cuales deben ejecutarse (por ejemplo, cuando el dispositivo está inactivo o cargándose).

La principal ventaja de BackgroundTasks es que le da a iOS un control mucho mayor sobre cuándo ejecutar las tareas, optimizando el consumo de energía. Ya no necesitas confiar en un fetch incierto o en notificaciones push silenciosas para la lógica de tu aplicación. Ahora puedes declarar explícitamente lo que tu app necesita hacer.

Tipos de Background Tasks

El framework BackgroundTasks define dos tipos principales de tareas:

  • BGAppRefreshTask: Ideal para actualizaciones rápidas de contenido. Similar a Background App Refresh, pero con más control y fiabilidad.
  • BGProcessingTask: Diseñado para tareas más largas y que consumen más recursos, como la sincronización de bases de datos, el procesamiento de medios o la carga de modelos de Machine Learning. Estas tareas solo se ejecutan cuando el dispositivo está inactivo y, opcionalmente, conectado a la corriente.

Implementación del BackgroundTasks Framework

Aquí hay una guía paso a paso para integrar BackgroundTasks en tu aplicación:

Paso 1: Habilitar Background Modes

En las Capabilities de tu proyecto, activa Background Modes y marca Background Fetch y Background Processing.

Paso 2: Declarar los Identificadores de Tareas

Ve a tu Info.plist (o a la pestaña Info del target en Xcode) y añade un nuevo array llamado Permitted background task scheduler identifiers. Añade una cadena para cada identificador de tarea que quieras registrar. Estos identificadores deben ser únicos y seguir una convención de nombrado inversa de dominio (ej: com.tuapp.refresh o com.tuapp.processData).

Info.plist Añadir fila (Add Row) Permitted background task scheduler identifiers (Tipo: Array) Añadir Item Item 0 (String) com.tuapp.refresh Configuración de tareas en segundo plano para iOS
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.tuapp.refresh</string>
    <string>com.tuapp.processData</string>
</array>

Paso 3: Registrar y Programar Tareas

En tu AppDelegate (o una clase similar que gestione el ciclo de vida de la aplicación), importa BackgroundTasks y registra tus tareas cuando la aplicación se inicie. Luego, programa las tareas cuando la aplicación vaya a segundo plano.

import UIKit
import BackgroundTasks

class AppDelegate: UIResponder, UIApplicationDelegate {

    let refreshTaskIdentifier = "com.tuapp.refresh"
    let processingTaskIdentifier = "com.tuapp.processData"

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        registerBackgroundTasks()
        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        scheduleAppRefresh()
        scheduleDataProcessing()
    }

    func registerBackgroundTasks() {
        BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshTaskIdentifier, using: nil) { task in
            // Este bloque se ejecuta cuando iOS lanza tu BGAppRefreshTask
            self.handleAppRefreshTask(task as! BGAppRefreshTask)
        }

        BGTaskScheduler.shared.register(forTaskWithIdentifier: processingTaskIdentifier, using: nil) { task in
            // Este bloque se ejecuta cuando iOS lanza tu BGProcessingTask
            self.handleDataProcessingTask(task as! BGProcessingTask)
        }
    }

    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: refreshTaskIdentifier)
        // Programa la próxima vez que te gustaría que se ejecute la tarea.
        // Ten en cuenta que esto es solo una sugerencia.
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // No antes de 15 minutos a partir de ahora

        do {
            try BGTaskScheduler.shared.submit(request)
            print("BGAppRefreshTask programada correctamente.")
        } catch {
            print("No se pudo programar la BGAppRefreshTask: \(error)")
        }
    }

    func scheduleDataProcessing() {
        let request = BGProcessingTaskRequest(identifier: processingTaskIdentifier)
        // Solo queremos que esta tarea se ejecute cuando el dispositivo esté inactivo y cargándose.
        request.requiresNetworkConnectivity = true // Requiere red
        request.requiresExternalPower = true     // Requiere corriente
        request.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) // No antes de 30 minutos a partir de ahora

        do {
            try BGTaskScheduler.shared.submit(request)
            print("BGProcessingTask programada correctamente.")
        } catch {
            print("No se pudo programar la BGProcessingTask: \(error)")
        }
    }

    func handleAppRefreshTask(_ task: BGAppRefreshTask) {
        // Programa una nueva tarea de actualización inmediatamente después de que esta termine.
        // Esto crea un ciclo recurrente.
        scheduleAppRefresh()

        // Define la operación principal que se ejecutará en segundo plano.
        // Asegúrate de que esta operación sea asíncrona y pueda ser cancelada.
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1

        task.expirationHandler = {
            // Se llama si el sistema decide terminar la tarea antes de tiempo.
            operationQueue.cancelAllOperations()
        }

        // Aquí iría tu lógica para actualizar contenido
        let refreshOperation = BlockOperation { [weak self] in
            guard let self = self else { return }
            print("Iniciando BGAppRefreshTask...")
            // Simula la descarga de datos
            Thread.sleep(forTimeInterval: 8) // Max 30s aprox, ser rápido!
            print("BGAppRefreshTask finalizada.")
            task.setTaskCompleted(success: true)
        }

        operationQueue.addOperation(refreshOperation)
    }

    func handleDataProcessingTask(_ task: BGProcessingTask) {
        scheduleDataProcessing() // Programa la próxima instancia

        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1

        task.expirationHandler = {
            operationQueue.cancelAllOperations()
        }

        // Lógica para procesar datos, sincronizar, etc.
        let processingOperation = BlockOperation { [weak self] in
            guard let self = self else { return }
            print("Iniciando BGProcessingTask (puede tomar más tiempo)... ")
            // Simula una tarea larga
            Thread.sleep(forTimeInterval: 25) // Puede ser más larga, hasta varios minutos en condiciones ideales
            print("BGProcessingTask finalizada.")
            task.setTaskCompleted(success: true)
        }

        operationQueue.addOperation(processingOperation)
    }
}
📌 Nota: Para depurar tareas en segundo plano en Xcode, puedes usar `Debug -> Simulate Background Task` en el menú. Esto te permite probar tus tareas sin tener que esperar a que el sistema las programe. Utiliza el identificador que definiste en `Info.plist`.

Testing Background Tasks en Xcode

Para probar tus BackgroundTasks en el simulador o en un dispositivo conectado a Xcode, después de que tu aplicación haya pasado a segundo plano, puedes usar el menú Debug > Simulate Background Task y seleccionar el identificador de tu tarea. Esto forzará la ejecución de la tarea, permitiéndote verificar que tu lógica funciona correctamente.


📍 Localización en Segundo Plano y Regiones (Location Background Modes)

Algunas aplicaciones, como las de navegación o las que rastrean actividad física, necesitan acceder a la ubicación del usuario incluso cuando la aplicación no está en primer plano. iOS proporciona un modo de segundo plano específico para esto.

Configuración

  1. Habilitar Location Updates: En las Capabilities de tu proyecto, activa Background Modes y marca Location Updates.

  2. Solicitar Permisos de Ubicación: Es fundamental solicitar el permiso Always para acceder a la ubicación en segundo plano. Esto se hace a través de CLLocationManager y añadiendo las claves NSLocationAlwaysUsageDescription y NSLocationWhenInUseUsageDescription (y NSLocationAlwaysAndWhenInUseUsageDescription en iOS 11+) en tu Info.plist.

import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()

override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
// Crucial para background location
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false

// Solicitar permiso "Always"
locationManager.requestAlwaysAuthorization()
}

func startTracking() {
locationManager.startUpdatingLocation()
print("Comenzando a rastrear ubicación en segundo plano...")
}

// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
print("Ubicación en segundo plano: lat=\(location.coordinate.latitude), lon=\(location.coordinate.longitude)")
// Procesa la nueva ubicación aquí
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Error de ubicación: \(error.localizedDescription)")
}

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print("Estado de autorización de ubicación cambiado a: \(status.rawValue)")
if status == .authorizedAlways {
startTracking()
}
}
}

Monitoring de Regiones (Geofencing)

El Core Location framework también permite monitorear regiones geográficas (geofencing) en segundo plano. Tu aplicación puede ser notificada cuando el usuario entra o sale de una región específica, incluso si la aplicación no está en ejecución. Esto es mucho más eficiente en términos de batería que el rastreo continuo.

// En tu LocationManager o clase similar
func startMonitoringRegions() {
    // Asegúrate de que el dispositivo soporta monitoring de regiones y que la autorización es Always
    guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else { return }
    guard locationManager.authorizationStatus == .authorizedAlways else { return }

    let regionCenter = CLLocationCoordinate2D(latitude: 37.3318, longitude: -122.0312) // Ejemplo: Sede de Apple
    let regionRadius: CLLocationDistance = 100.0 // 100 metros
    let region = CLCircularRegion(center: regionCenter, radius: regionRadius, identifier: "AppleCampus")

    region.notifyOnEntry = true
    region.notifyOnExit = true

    locationManager.startMonitoring(for: region)
    print("Monitoreando región: \(region.identifier)")
}

// Delegate method para cuando se detecta entrada/salida de región
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    print("Entró en la región: \(region.identifier)")
    // Envía una notificación local o actualiza UI, etc.
}

func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    print("Salió de la región: \(region.identifier)")
}
💡 Consejo: Utiliza el monitoring de regiones en lugar de actualizaciones de ubicación continuas siempre que sea posible para ahorrar batería. El número de regiones que puedes monitorear simultáneamente es limitado (20 por aplicación).

📞 VoIP y Audio en Segundo Plano

Ciertas categorías de aplicaciones tienen permisos especiales para operar en segundo plano debido a su naturaleza. Estas incluyen aplicaciones de Voz sobre IP (VoIP) y aplicaciones de audio que reproducen o graban en segundo plano.

Audio en Segundo Plano

Si tu aplicación reproduce audio (música, podcasts) o graba audio (grabadora de voz) y necesita seguir haciéndolo cuando el usuario cambia a otra aplicación o bloquea el dispositivo, debes habilitar el modo de segundo plano Audio, AirPlay, and Picture in Picture.

  1. Habilitar Background Modes: En las Capabilities de tu proyecto, activa Background Modes y marca Audio, AirPlay, and Picture in Picture.

  2. Configurar la Sesión de Audio: Es crucial configurar correctamente la sesión de audio (AVAudioSession) con una categoría apropiada, como .playback o .record, y activarla.

import AVFoundation

class AudioManager: NSObject {

static let shared = AudioManager()
private override init() { super.init() }

func setupAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default, options: [])
try audioSession.setActive(true)
print("Audio session configurada para reproducción en segundo plano.")
} catch {
print("Error al configurar la sesión de audio: \(error.localizedDescription)")
}
}

func playBackgroundAudio() {
setupAudioSession()
// Inicia la reproducción de tu audio aquí
print("Reproduciendo audio en segundo plano...")
// Ejemplo: un AVPlayer
// let url = URL(string: "https://example.com/background_sound.mp3")!
// let player = AVPlayer(url: url)
// player.play()
}
}

VoIP en Segundo Plano

Las aplicaciones de VoIP pueden mantener una conexión de red abierta en segundo plano para recibir llamadas entrantes. Esto se logra habilitando el modo de segundo plano Voice over IP.

  1. Habilitar Background Modes: En las Capabilities de tu proyecto, activa Background Modes y marca Voice over IP.

  2. Configurar la Sesión de Audio para VoIP: Utiliza la categoría .playAndRecord y asegúrate de que tu aplicación maneje las notificaciones push de VoIP para "despertarse" y aceptar llamadas.

// Configuración similar a audio, pero con diferente categoría
func setupVoIPAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .mixWithOthers])
try audioSession.setActive(true)
print("Audio session configurada para VoIP en segundo plano.")
} catch {
print("Error al configurar la sesión de audio para VoIP: \(error.localizedDescription)")
}
}
🔥 Importante: Abusar de estos modos de segundo plano, especialmente el de VoIP, sin una funcionalidad principal que lo justifique, puede llevar a que Apple rechace tu aplicación en la App Store o a una penalización de la batería por parte del sistema.

📊 Gestión de Descargas en Segundo Plano con URLSession

Para descargar archivos grandes o múltiples archivos de manera confiable en segundo plano, iOS proporciona el URLSession con un tipo de configuración especial: background. Este tipo de sesión puede continuar las descargas incluso si la aplicación es suspendida o terminada por el sistema, o incluso si el dispositivo se reinicia.

Cuando la descarga finaliza, iOS relanza (o despierta) tu aplicación en segundo plano, dándole la oportunidad de procesar el archivo descargado. Esto es fundamental para aplicaciones que manejan contenido multimedia, libros electrónicos, actualizaciones de mapas, etc.

Implementación de URLSession en Segundo Plano

  1. Configurar URLSessionConfiguration: Crea una configuración de sesión de segundo plano con un identificador único.
import Foundation

class BackgroundDownloader: NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

static let shared = BackgroundDownloader()
var backgroundSession: URLSession!
var completionHandler: (() -> Void)? // Para AppDelegate para manejar el relanzamiento

private override init() {
super.init()
// El identificador debe ser único para tu aplicación y para cada sesión de fondo
let configuration = URLSessionConfiguration.background(withIdentifier: "com.tuapp.backgrounddownloader")
// Puedes configurar otras propiedades, como permiteCellularAccess, timeoutIntervalForResource, etc.
configuration.allowsCellularAccess = true
backgroundSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}

func startDownload(from url: URL) {
let downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
print("Iniciando descarga en segundo plano para: \(url.lastPathComponent)")
}

// MARK: - URLSessionDownloadDelegate

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Descarga completada para: \(downloadTask.originalRequest?.url?.lastPathComponent ?? "") a \(location.path)")

// Mueve el archivo descargado a una ubicación permanente antes de que el sistema lo borre
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsURL.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)

do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: location, to: destinationURL)
print("Archivo movido a: \(destinationURL.path)")
// Aquí puedes notificar a tu UI o procesar el archivo
} catch {
print("Error al mover el archivo: \(error)")
}
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
print("Progreso de descarga para \(downloadTask.originalRequest?.url?.lastPathComponent ?? ""): \(progress * 100)% ")
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Error de descarga: \(error.localizedDescription)")
} else {
print("Tarea de sesión completada sin errores.")
}
}

// MARK: - URLSessionDelegate

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("Todos los eventos de la sesión de segundo plano han terminado.")
// Llama al completion handler que el AppDelegate nos proporcionó
DispatchQueue.main.async {
self.completionHandler?()
self.completionHandler = nil
}
}
}
  1. Manejar el Relanzamiento de la Aplicación en AppDelegate: Cuando una sesión de URL en segundo plano completa todas sus tareas (o la aplicación es relanzada por iOS debido a esto), el método application(_:handleEventsForBackgroundURLSession:completionHandler:) en tu AppDelegate es invocado. Es crucial capturar el completionHandler y pasárselo a tu URLSession.
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

// ... otros métodos

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
print("Manejando eventos para sesión de fondo con identificador: \(identifier)")
// Asegúrate de que tu URLSession se re-inicialice con el mismo identificador
BackgroundDownloader.shared.completionHandler = completionHandler
}

// Para iniciar una descarga (por ejemplo, desde un controlador de vista)
func downloadExampleFile() {
let fileURL = URL(string: "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4")!
BackgroundDownloader.shared.startDownload(from: fileURL)
}
}
💡 Consejo: Asegúrate de que el `completionHandler` en `application(_:handleEventsForBackgroundURLSession:completionHandler:)` sea llamado solo después de que todas las descargas y el procesamiento asociado hayan terminado. No liberarlo a tiempo puede causar que tu aplicación sea terminada por iOS.

📈 Monitoreo de Uso de Datos y Batería

La gestión de tareas en segundo plano implica una responsabilidad importante: la eficiencia en el uso de los recursos. Un uso indebido o excesivo de los recursos del sistema (CPU, red, batería) mientras la aplicación está en segundo plano puede tener consecuencias negativas, incluyendo un bajo rendimiento, una mala calificación en la App Store y, en casos extremos, la terminación de la aplicación por parte de iOS.

Herramientas de Xcode

Xcode proporciona herramientas valiosas para monitorear el rendimiento y el uso de recursos de tu aplicación:

  • Energy Impact: En el Debug Navigator de Xcode, puedes ver el impacto energético de tu aplicación en tiempo real. Esto es crucial para identificar tareas en segundo plano que consumen demasiada batería.
  • CPU Usage: Monitorea el uso de CPU para asegurarte de que tus tareas en segundo plano no estén acaparando recursos excesivamente.
  • Network Usage: Observa la actividad de red para detectar descargas o subidas de datos innecesarias o prolongadas.
  • Instruments: La herramienta Instruments (especialmente las plantillas Energy Log y Activity Monitor) es indispensable para un análisis más profundo del consumo de energía y el rendimiento general de tu aplicación, incluyendo la actividad en segundo plano.

Buenas Prácticas

  • Ser lo más eficiente posible: Minimiza la cantidad de trabajo que se realiza en segundo plano. Si una tarea puede esperar hasta que el usuario abra la aplicación, haz que espere.
  • Consolidar operaciones de red: Si necesitas hacer varias llamadas a la red, intenta agruparlas para reducir la frecuencia de activación de la red.
  • Usar condiciones apropiadas: Para BGProcessingTask, utiliza requiresNetworkConnectivity y requiresExternalPower para asegurarte de que las tareas largas solo se ejecuten cuando las condiciones sean óptimas.
  • Caché inteligente: Descarga y almacena en caché solo los datos necesarios. Evita la descarga repetida del mismo contenido.
  • Pruebas exhaustivas: Prueba tus tareas en segundo plano en diferentes condiciones de red, batería y uso para asegurarte de que se comportan como esperas.
⚠️ Advertencia: Una aplicación que constantemente consume batería en segundo plano será notificada al usuario en la configuración de batería de iOS y podría ser eliminada si el problema persiste.

✅ Conclusión y Mejores Prácticas

Las tareas en segundo plano son un componente vital para el desarrollo de aplicaciones iOS modernas, permitiéndote ofrecer experiencias de usuario ricas y actualizadas. Sin embargo, el poder de la ejecución en segundo plano viene con la responsabilidad de usar los recursos del sistema de manera inteligente y eficiente. Ignorar esta responsabilidad puede llevar a una mala experiencia de usuario, un alto consumo de batería y, en última instancia, el rechazo o la desinstalación de tu aplicación.

Hemos explorado una variedad de técnicas, desde los métodos tradicionales de solicitud de tiempo de ejecución hasta los frameworks más avanzados como BackgroundTasks y URLSession para descargas en segundo plano. Cada método tiene sus propios casos de uso y limitaciones, y la elección correcta depende de la naturaleza de la tarea que tu aplicación necesita realizar.

Resumen de Estrategias:

EstrategiaCaso de Uso PrincipalConsumo de BateríaiOS VersiónNotas
---------------
beginBackgroundTaskFinalizar tareas cortas al ir a segundo planoBajo (si es corto)iOS 4+Tiempo limitado (aprox. 30s), debe llamar a endTask.
Background App RefreshActualización de contenido regular (noticias, clima)ModeradoiOS 7+Decidido por el sistema, no garantizado.
---------------
Push con content-availableDespertar la app para actualizar contenido tras eventoModeradoiOS 7+Requiere servidor, puede ser penalizado si es excesivo.
BackgroundTasks (BGAppRefreshTask)Actualización de contenido inteligente y controladaModeradoiOS 13+Más confiable que Background App Refresh legacy.
---------------
BackgroundTasks (BGProcessingTask)Tareas largas/intensivas (sync, ML)Moderado-AltoiOS 13+Requiere condiciones específicas (inactivo, cargando).
Location UpdatesRastreo GPS continuo (navegación, fitness)AltoiOS 4+Requiere permiso Always, alto consumo si no se optimiza.
---------------
Region MonitoringGeofencing (entradas/salidas de zonas)BajoiOS 4+Mucho más eficiente que el rastreo continuo.
Audio/VoIP ModesReproducción/grabación de audio, llamadas VoIPModeradoiOS 4+Necesario para apps de audio/llamadas, requiere AVAudioSession.
---------------
URLSession BackgroundDescargas de archivos grandes/múltiplesModeradoiOS 7+Muy fiable, descarga continúa incluso si la app se cierra.

Al aplicar estas técnicas, siempre prioriza la experiencia del usuario y la salud de la batería del dispositivo. Desarrollar un comportamiento responsable en segundo plano es una marca de calidad para cualquier aplicación iOS. ¡Ahora estás equipado para dominar las tareas en segundo plano y crear aplicaciones verdaderamente excepcionales!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!