tutoriales.com

¡Maestría en Handoff! Continuidad de Actividades en iOS con SwiftUI y UIKit

Este tutorial te guiará a través de la implementación de Handoff en tus aplicaciones iOS, permitiendo a los usuarios continuar una actividad desde un dispositivo Apple a otro. Aprenderás a configurar y manejar NSUserActivity tanto con SwiftUI como con UIKit, ofreciendo una experiencia de usuario fluida y conectada. Prepárate para llevar la continuidad de tus apps al siguiente nivel.

Intermedio18 min de lectura8 views
Reportar error

La experiencia de usuario moderna en el ecosistema Apple se define por la fluidez y la interconexión. Una de las características más potentes que habilita esta continuidad es Handoff. ¿Alguna vez has empezado a escribir un email en tu iPhone y lo has terminado sin problemas en tu Mac? Eso es Handoff en acción. Este tutorial te sumergirá en el mundo de Handoff, mostrándote cómo implementarlo en tus propias aplicaciones iOS, ya sea que uses SwiftUI o UIKit.

🚀 ¿Qué es Handoff y por qué es importante?

Handoff, parte de la funcionalidad de Continuidad de Apple, permite a los usuarios transferir una actividad que están realizando en una aplicación de un dispositivo a la misma aplicación (o una compatible) en otro dispositivo Apple cercano. Esto crea una experiencia de usuario seamless o sin interrupciones, mejorando la productividad y la satisfacción del usuario.

Imagina un usuario leyendo un artículo en tu app de noticias en su iPhone mientras viaja. Al llegar a casa, puede sentarse frente a su iPad o Mac y, con un solo toque o clic, continuar leyendo el artículo exactamente donde lo dejó. ¡Esto es el poder de Handoff!

💡 Consejo: Handoff no solo se limita a iPhone y Mac. Funciona con iPad, Apple Watch y Apple TV, creando un ecosistema de continuidad total.

Beneficios Clave de Implementar Handoff:

  • Experiencia de Usuario Mejorada: Ofrece una interacción fluida y sin fricciones entre dispositivos.
  • Mayor Productividad: Los usuarios pueden cambiar de dispositivo sin perder el contexto de su trabajo.
  • Diferenciación de la App: Destaca tu aplicación al integrar una característica premium del ecosistema Apple.
  • Engagement: Fomenta un mayor uso de tu aplicación en diferentes contextos y dispositivos.

⚙️ Entendiendo NSUserActivity: El Corazón de Handoff

El componente central para implementar Handoff es NSUserActivity. Este objeto es el encargado de encapsular la información sobre la actividad actual del usuario, como el tipo de actividad, un título descriptivo y un userInfo (un diccionario que contiene datos específicos de la actividad, como un ID de documento o una URL).

Cuando una actividad se pasa a otro dispositivo, NSUserActivity se transmite, y la aplicación receptora utiliza la información contenida en él para restaurar el estado de la actividad.

Propiedades clave de NSUserActivity:

PropiedadDescripciónTipo
---------
activityTypeUn identificador único para el tipo de actividad. Debe ser una cadena.String
titleUn título descriptivo de la actividad, visible para el usuario.String?
---------
userInfoUn diccionario con datos de la actividad para su restauración.[AnyHashable: Any]?
isEligibleForHandoffBooleano que indica si la actividad puede ser entregada a otros dispositivos.Bool
---------
becomeCurrent()Marca esta actividad como la actividad actual del usuario.func
invalidate()Invalida la actividad, indicando que ya no es relevante.func
🔥 Importante: Para que Handoff funcione correctamente, el `activityType` debe ser registrado en el archivo `Info.plist` de tu proyecto.
Dispositivo A (App) Crea NSUserActivity Establece isEligibleForHandoff = true Llama becomeCurrent() iCloud / Bluetooth Dispositivo B (App) AppDelegate/SceneDelegate recibe Activity Restaura estado de actividad EMISOR RECEPTOR

🛠️ Preparando tu Proyecto para Handoff

Antes de escribir cualquier línea de código, necesitas configurar tu proyecto.

1. Habilitar Capacidades (Capabilities)

Ve a tu proyecto en Xcode, selecciona el target de tu aplicación, luego la pestaña Signing & Capabilities y haz clic en + Capability. Busca Handoff y añádelo.

Esto es crucial para que tu aplicación pueda participar en el proceso de Handoff.

2. Registrar Tipos de Actividad en Info.plist

Para cada tipo de actividad que quieras soportar con Handoff, debes registrarlo. Abre tu archivo Info.plist (o la pestaña Info de tu target) y añade una nueva entrada:

  • Key: NSUserActivityTypes
  • Type: Array

Dentro de este array, añade un item por cada activityType que vayas a usar. Por ejemplo, si tu app tiene una actividad para 'ver artículo' y otra para 'crear nota', añadirías dos strings:

  • com.tuempresa.tuapp.viewArticle
  • com.tuempresa.tuapp.createNote

Es una buena práctica usar un identificador único y legible, generalmente con el formato de nombre de dominio inverso.

<key>NSUserActivityTypes</key>
<array>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).viewArticle</string>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).createNote</string>
</array>
📌 Nota: Usar `$(PRODUCT_BUNDLE_IDENTIFIER)` asegura que el prefijo del tipo de actividad coincida con el identificador de tu aplicación.

🧑‍💻 Implementación de Handoff con SwiftUI

SwiftUI ha simplificado la integración de NSUserActivity a través de modificadores de vista. Vamos a ver cómo implementar una actividad para ver un artículo.

1. Definir el Tipo de Actividad

Primero, definimos una extensión para nuestro tipo de actividad para evitar errores tipográficos y mejorar la legibilidad.

extension NSUserActivity.ActivityType {
    static let viewArticle = NSUserActivity.ActivityType("viewArticle")
}

Este viewArticle es lo que concatenaremos con PRODUCT_BUNDLE_IDENTIFIER en Info.plist.

2. Crear y Actualizar la Actividad en una Vista SwiftUI

Dentro de una vista SwiftUI, puedes usar el modificador .userActivity() para crear y mantener actualizada una NSUserActivity.

Considera una ArticleView que muestra el contenido de un artículo.

import SwiftUI

struct Article {
    let id: String
    let title: String
    let content: String
}

struct ArticleView: View {
    let article: Article

    var body: some View {
        ScrollView {
            Text(article.title)
                .font(.largeTitle)
                .padding()
            Text(article.content)
                .padding()
        }
        .navigationTitle(article.title)
        .userActivity(NSUserActivity.ActivityType.viewArticle) { userActivity in
            userActivity.title = "Viendo Artículo: \(article.title)"
            userActivity.userInfo = ["articleID": article.id]
            userActivity.isEligibleForHandoff = true
        }
    }
}

En este ejemplo:

  • userActivity es una NSUserActivity recién creada o ya existente de tipo .viewArticle.
  • Configuramos el title para que sea descriptivo.
  • Establecemos userInfo con el articleID, que es la pieza clave de información que necesitaremos para restaurar el estado en otro dispositivo.
  • isEligibleForHandoff se establece en true para indicar que esta actividad puede ser transferida.
⚠️ Advertencia: El `userInfo` debe contener solo tipos de datos que se puedan serializar a `PropertyList` (como `String`, `Int`, `Double`, `Date`, `Data`, `Array`, `Dictionary`). Si necesitas pasar objetos complejos, primero deberás codificarlos a `Data`.

3. Manejar la Actividad Entrante en SwiftUI (App)

Para recibir una actividad de Handoff, necesitas manejar el evento en tu archivo App principal.

import SwiftUI

@main
struct HandoffSwiftUIApp: App {
    @State private var activeArticleID: String? // Para navegar a la vista correcta

    var body: some Scene {
        WindowGroup {
            ContentView(activeArticleID: $activeArticleID)
                .onContinueUserActivity(NSUserActivity.ActivityType.viewArticle) { userActivity in
                    handleHandoffActivity(userActivity)
                }
        }
    }

    private func handleHandoffActivity(_ userActivity: NSUserActivity) {
        if userActivity.activityType == NSUserActivity.ActivityType.viewArticle.rawValue {
            if let articleID = userActivity.userInfo?["articleID"] as? String {
                print("Handoff received for article ID: \(articleID)")
                activeArticleID = articleID
            } else {
                print("Error: No article ID found in userInfo")
            }
        }
    }
}

En App:

  • Usamos el modificador .onContinueUserActivity() y especificamos el tipo de actividad que queremos manejar.
  • Dentro del closure, extraemos el articleID de userInfo.
  • Actualizamos una @State variable (activeArticleID) que luego se puede usar para navegar a la ArticleView correcta, quizás usando un NavigationLink o un sheet condicional.

4. Mostrar la Vista del Artículo Restaurada

Tu ContentView (o la vista principal que maneja la navegación) necesitará observar activeArticleID y presentar la ArticleView apropiada.

struct ContentView: View {
    @Binding var activeArticleID: String?
    // Suponemos que tienes una forma de buscar artículos por ID
    @State private var selectedArticle: Article? // Para presentar el detalle

    var body: some View {
        NavigationView {
            List {
                // ... Lista de artículos para navegación normal
                Text("Artículo 1").onTapGesture { activeArticleID = "article-1" }
                Text("Artículo 2").onTapGesture { activeArticleID = "article-2" }
            }
            .navigationTitle("Mis Artículos")
            .sheet(item: $selectedArticle) { article in
                ArticleView(article: article)
            }
            .onChange(of: activeArticleID) { newArticleID in
                if let id = newArticleID {
                    // Aquí deberías cargar el artículo real por ID
                    // Por simplicidad, usamos datos mock
                    if id == "article-1" {
                        selectedArticle = Article(id: "article-1", title: "El Arte del Handoff", content: "Contenido del artículo 1...")
                    } else if id == "article-2" {
                        selectedArticle = Article(id: "article-2", title: "SwiftUI Avanzado", content: "Contenido del artículo 2...")
                    }
                    activeArticleID = nil // Limpiar después de procesar
                }
            }
        }
    }
}
🔥 Importante: La lógica para buscar y cargar el `Article` real basada en `articleID` es crucial. En una app real, esto implicaría una llamada a una base de datos o API.

🧑‍💻 Implementación de Handoff con UIKit

En UIKit, la integración de Handoff se realiza principalmente a través del AppDelegate o SceneDelegate y la sobrescritura de métodos en tus UIViewControllers.

1. Crear y Actualizar la Actividad en UIViewController

Cada UIViewController que represente una actividad 'handoffable' debe tener una NSUserActivity asociada y mantenerla actualizada.

import UIKit

class ArticleViewController: UIViewController {
    var article: Article? // Asume que Article es una struct definida como antes
    var userActivity: NSUserActivity?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupUserActivity()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        userActivity?.becomeCurrent()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        userActivity?.invalidate()
    }

    private func setupUserActivity() {
        guard let article = article else { return }
        
        let activityType = "\(Bundle.main.bundleIdentifier!).viewArticle"
        userActivity = NSUserActivity(activityType: activityType)
        userActivity?.title = "Viendo Artículo: \(article.title)"
        userActivity?.userInfo = ["articleID": article.id]
        userActivity?.isEligibleForHandoff = true
        userActivity?.needsSave = true // Importante para que el sistema lo actualice
        
        // Asigna la actividad a la propiedad userActivity del UIViewController
        self.userActivity = userActivity
    }

    override func updateUserActivityState(_ activity: NSUserActivity) {
        // Este método se llama cuando el sistema necesita actualizar el estado de la actividad.
        // Puedes actualizar userInfo aquí si el estado ha cambiado.
        if let article = article {
            activity.userInfo?["articleID"] = article.id
            // Por ejemplo, si tu artículo tiene una posición de scroll, la podrías guardar aquí
            // activity.userInfo?["scrollPosition"] = scrollView.contentOffset.y
        }
        super.updateUserActivityState(activity)
    }

    private func setupUI() {
        // Configura tu UI para mostrar el artículo
        view.backgroundColor = .white
        let titleLabel = UILabel()
        titleLabel.text = article?.title
        titleLabel.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        let contentLabel = UILabel()
        contentLabel.text = article?.content
        contentLabel.numberOfLines = 0
        contentLabel.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(titleLabel)
        view.addSubview(contentLabel)

        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),

            contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
            contentLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            contentLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
        ])
    }
}

En este código UIKit:

  • Sobrescribimos userActivity en el UIViewController para asociar la actividad.
  • setupUserActivity() inicializa NSUserActivity con el tipo correcto, título y userInfo (el articleID).
  • becomeCurrent() se llama en viewDidAppear para indicar que esta actividad es la que el usuario está realizando activamente.
  • invalidate() se llama en viewWillDisappear para decir que la actividad ya no es relevante si el usuario sale de la vista.
  • updateUserActivityState() se usa para actualizar userInfo si el estado de la actividad cambia mientras la vista está activa.

2. Manejar la Actividad Entrante en UIKit (SceneDelegate)

En aplicaciones modernas de UIKit con Scenes, el manejo de Handoff se realiza en el SceneDelegate.

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        guard userActivity.activityType == "\(Bundle.main.bundleIdentifier!).viewArticle",
              let articleID = userActivity.userInfo?["articleID"] as? String else {
            return
        }

        print("Handoff received for article ID: \(articleID)")
        
        // Aquí necesitas restaurar el estado de tu UI para mostrar el artículo.
        // Esto típicamente implica encontrar el NavigationController raíz y hacer un push/present.
        if let navigationController = window?.rootViewController as? UINavigationController {
            // Suponemos que tienes una forma de buscar artículos por ID
            let mockArticle = Article(id: articleID, title: "Artículo Restaurado", content: "Contenido del artículo restaurado desde Handoff.")
            let articleVC = ArticleViewController()
            articleVC.article = mockArticle
            navigationController.pushViewController(articleVC, animated: true)
        } else if let mainTabBarController = window?.rootViewController as? UITabBarController {
            // Si tienes un TabBarController, podrías seleccionar una pestaña específica
            // y luego navegar dentro de ella.
            if let navVC = mainTabBarController.selectedViewController as? UINavigationController {
                let mockArticle = Article(id: articleID, title: "Artículo Restaurado", content: "Contenido del artículo restaurado desde Handoff.")
                let articleVC = ArticleViewController()
                articleVC.article = mockArticle
                navVC.pushViewController(articleVC, animated: true)
            }
        }
    }
}
  • El método scene(_:continue:) es invocado cuando tu aplicación recibe una NSUserActivity a través de Handoff.
  • Verificamos el activityType y extraemos el articleID de userInfo.
  • La lógica para restaurar la interfaz de usuario dependerá de la estructura de tu aplicación (por ejemplo, empujar un nuevo UIViewController en un UINavigationController).
📌 Nota: Si tu aplicación usa un `AppDelegate` (para apps que no soportan Scenes o en iOS 12 y anteriores), el método análogo es `application(_:continue:restorationHandler:)`.

✅ Buenas Prácticas y Consideraciones Adicionales

Identificadores Únicos y Consistentes

Asegúrate de que tus activityType y las claves en userInfo sean consistentes en todos tus dispositivos y versiones de la aplicación. Un cambio en estos podría romper la funcionalidad de Handoff.

Datos de userInfo

  • Solo lo Esencial: Incluye solo la información mínima necesaria para restaurar el estado de la actividad (por ejemplo, un ID, una URL). No intentes pasar objetos de datos complejos o grandes bloques de texto.
  • Serializables: Los valores en userInfo deben ser serializables a PropertyList (String, Number, Date, Data, Array, Dictionary). Para objetos personalizados, codifícalos a Data.

needsSave

En UIKit, establecer userActivity.needsSave = true es importante para indicarle al sistema que debe llamar a updateUserActivityState() en algún momento para obtener la información más reciente de la actividad. En SwiftUI, el closure del modificador .userActivity() se encarga de esto automáticamente cuando el estado relevante cambia.

Gestionar el Ciclo de Vida de la Actividad

  • Llama a becomeCurrent() cuando la actividad sea la principal en pantalla.
  • Llama a invalidate() cuando la actividad ya no sea relevante o el usuario la haya abandonado.

Prueba Rigurosa

Prueba Handoff en múltiples dispositivos (iPhone, iPad, Mac si tu app es multiplataforma) y escenarios (app en segundo plano, app cerrada, red lenta) para asegurar un comportamiento robusto.

¿Qué pasa si la app receptora no está instalada?Si la aplicación receptora no está instalada en el dispositivo de destino, el sistema operativo puede ofrecer al usuario abrir la App Store para descargarla, o mostrar una URL si la `webpageURL` de la actividad está configurada.

Web Handoff (Universal Links)

Para una experiencia de continuidad aún más robusta, puedes combinar Handoff con webpageURL en NSUserActivity y Universal Links. Esto permite que una actividad de Handoff que no pueda ser procesada por la app (quizás porque no está instalada o es una versión antigua), abra una página web relevante en Safari.

userActivity.webpageURL = URL(string: "https://www.tuempresa.com/articles/\(article.id)")

Configurar Universal Links es un tema más amplio, pero complementa perfectamente Handoff al ofrecer una opción de fallback basada en web.

Depuración de Handoff

La depuración puede ser un desafío. Puedes usar el Console.app en macOS y filtrar por activity para ver los logs relacionados con NSUserActivity y Handoff. Asegúrate también de que ambos dispositivos estén iniciados sesión con la misma cuenta de iCloud y que Bluetooth y Wi-Fi estén activados.

📈 Futuro de Handoff y Continuidad

Apple continúa invirtiendo fuertemente en su ecosistema de Continuidad. Características como Continuity Camera, Universal Clipboard y Sidecar son extensiones de esta visión. Dominar Handoff no solo te permite construir mejores aplicaciones hoy, sino que también te prepara para integrar futuras innovaciones de continuidad.

100% Dominado

Conclusión ✨

Handoff es una característica poderosa que eleva la experiencia del usuario en el ecosistema Apple. Al integrar NSUserActivity de manera efectiva, ya sea con SwiftUI o UIKit, puedes permitir que tus usuarios transfieran actividades sin problemas entre sus dispositivos, creando una interacción verdaderamente conectada y productiva. ¡Ahora tienes las herramientas para implementar esta funcionalidad y llevar tus aplicaciones iOS al siguiente nivel de sofisticación y usabilidad!

Tutoriales relacionados

Comentarios (0)

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