- Introducción
- Cómo declarar Scene Delegate
- Nueva forma de manejar los estados en Swift UI (iOS14)
- Adaptación de la app a nueva API de escenas
- Los métodos para crear las ventanas
- Actualización del contenido de la ventanas
- Conclusiones
- Referencias
Introducción
Con la introducción de iOS 13 en iPad OS por fin tenemos la posibilidad de crear y usar las ventanas en nuestra app de iOS. En este tutorial lo aprendemos. Históricamente las aplicaciones iOS usaban solo una ventana. Cada ventana puede contener la información completamente diferente o parecida. Para implementarlo no tenemos que hacer mucho esfuerzo. El proceso es bastante sencillo y claro. De momento solo los usuarios de iPad pueden disfrutar de esta funcionalidad, pero seguramente en el futuro podemos verlo en los iPhones también.
Cómo declarar Scene Delegate
Apple ha cambiado significativamente el ciclo de vida de aplicación en iOS 13. Antes cada aplicación tenía solo una ventana y su ciclo de vida fue gestionado por UIApplicationDelegate. Pero en iOS13 cada aplicación puede tener diferentes ventanas (escenas) y hay que gestionar el ciclo de vida de cada una. Ahora UISession controla el ciclo de vida de ventana. También hay nuevos protocolos para manejar el ciclo de vida de la ventana: UISceneSession, UISceneDelegate y UISceneConfiguration.
Estas novedades cambian la forma en que interactúa con UIApplicationDelegate. Muchos de los métodos de UIApplicationDelegate ahora se han trasladado al delegado de escena UIWindowSceneDelegate.
import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. let contentView = ContentView() // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } } func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } }
Como veis son prácticamente iguales a los métodos del protocolo UIApplicationDelegate. En iOS 13 en UIApplicationDelegate tenemos dos nuevos métodos para manejar las sesiones.
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } }
Se usan para configurar las sesiones (el rol de la sesión: la ventana o el documento, la jerarquía de la vistas) y para eliminar todos los recursos de las sesiones descartadas.
Nueva forma de manejar los estados en Swift UI (iOS14)
En las ultimas betas de Xcode 12, hay una nueva opción al crear una aplicación SwiftUI: «SwiftUI App«. Es una de las dos opciones para el ciclo de vida de la aplicación. Ahora podemos controlar el ciclo de la sesión con el código declarativo de SwiftUI. Es una manera mucho más compacta y clara.
@main struct HelloWorldApp: App { @Environment(\.scenePhase) private var scenePhase @SceneBuilder var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { (newScenePhase) in switch newScenePhase { case .active: print("scene is now active!") case .inactive: print("scene is now inactive!") case .background: print("scene is now in the background!") @unknown default: print("Apple must have added something new!") } } Settings { SettingsView() } } }
Antes, esto se manejaba en AppDelegate y SceneDelegate (dos ficheros distintos), lo que dificultaba la administración del estado de la aplicación.
Tenemos que poner el property wrapper @SceneBuilder a la propiedad body si queremos incluir más que una escena en la app.
Además, ahora los ficheros AppDelegate y SceneDelegate no están en el proyecto. En cambio, un archivo llamado <App Name> App los reemplaza.
Las novedades de nueva estructura
Dentro de este fichero hay algunas novedades:
- @main le dice a Xcode que la siguiente estructura, HelloWorldApp, será el punto de entrada para la aplicación. Solo se puede marcar una estructura con este atributo.
- Según la documentación, App es un protocolo que «representa la estructura y el comportamiento de una aplicación». HelloWorldApp se ajusta a esto. Es como la vista base de aplicación. Literalmente, aquí escribes cómo se verá la aplicación.
- Scene: el body de una vista SwiftUI debe ser del tipo Vista. Del mismo modo, el cuerpo de una aplicación SwiftUI debe ser del tipo UIScene.
Cada escena contiene la vista raíz de una jerarquía de vistas y tiene un ciclo de vida administrado por el sistema. La escena actúa como un contenedor para sus vistas. El sistema decide cuándo y cómo presentar la jerarquía de vistas en la interfaz de usuario de una manera apropiada para la plataforma y que dependa del estado actual de la aplicación.
Y como las plataforma macOS y iPadOS admiten múltiples ventanas, ajustar todas las vistas de aplicación en una escena facilita la reutilización al tiempo que permite «fases de escena» que incluyen estados activos, inactivos y de fondo.
WindowGroup es una escena que envuelve vistas. La vista que queremos presentar, (ContentView) es una Vista, no una escena. WindowGroup nos permite envolverlos en una sola escena que SwiftUI puede reconocer y mostrar.
Primero hacemos una propiedad, scenePhase, que obtiene el estado actual de actividad del sistema. La barra invertida (\) indica que estamos usando un keypath, lo que significa que nos estamos refiriendo a la propiedad en sí y no a su valor. Y cada vez que cambia el valor de la propiedad, se llama al modificador onChange, donde podemos obtener el estado del ciclo de vida.
Adaptación de app a nueva API de escenas
Para añadir el soporte de múltiples ventanas a tu aplicación hay que actualizar el fichero Info.plist. Los siguientes pasos para actualizar el fichero Info.plist:
- Abre Info.plist
- Pincha el botón «+» y añade el nodo Application Scene Manifest.
- Abre el elemento Application Scene Manifest haciendo clic en el botón (▼).
- Establece el valor de «Enable Multiple Windows» en «YES».
- Abre el elemento «Scene Configuration», pinchando el botón (+) para añadir la nueva configuración de escena
- Elige «Application Session Role».
- Abre el primer elemento «Item 0» que contiene los valores como «Class Name«, «Delegate Class Name«, «Configuration Name» y «Storyboard Name«. Tienes que rellenar «Delegate Class Name» con el nombre de la clase del delegado de tu escena (por ejemplo, $(PRODUCT_MODULE_NAME).SceneDelegate), «Configuration Name» con un nombre único que la aplicación utilizará para identificar la escena internamente, «Storyboard Name» con el nombre del storyboard que contiene la interfaz de usuario inicial de la escena (borrarlo si usas SwiftUI) y «Class Name» con el nombre de la clase de escena que habitualmente es UIWindowScene (normalmente puedes borrarlo también).
Después de haber establecido estos valores, tienes que añadir un delegado de escena.
Añadir un delegado de escena
El nombre de clase de este delegado de escena debe coincidir con el nombre de clase que has puesto en el archivo Info.plist en el paso anterior.
Crea un nuevo archivo Swift llamado SceneDelegate.swift y añade el siguiente código al nuevo archivo:
import UIKit @available(iOS 13.0, *) class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController( rootView: ContentView() ) self.window = window window.makeKeyAndVisible() } } }
Si necesitas el soporte de iOS 12, añade el modificador @available(iOS 13.0, *). Solo se compilará para iOS 13.
Tienes que añadir la propiedad window.
Es posible que tengas que reproducir parte de la lógica de tu aplicación existente en tu nuevo delegado de escena. En iOS 13, el delegado de escena realiza muchas funciones que realizaba UIApplicationDelegate.
Si solo quieres la compatibilidad con iOS 13 y versiones posteriores, mueve esta lógica. Si es compatible con iOS 13 y sistemas operativos anteriores, deja el código de UIApplicationDelegate tal cual y también añádelo al delegado de escena. Como ves el delegado de escena duplica en parte la funcionalidad de delegado de aplicación (UIApplicationDelegate).
Con eso ya podemos crear nuevas ventanas usando tres diferentes métodos.
Los métodos para crear diferentes ventanas
Un usuario puede crear las nuevas ventanas usando varios métodos diferentes:
Si la aplicación declara compatibilidad con múltiples ventanas, el usuario puede entrar en la vista multitarea deslizando hacia arriba para mostrar el dock y pulsando el icono de su aplicación mientras la aplicación ya está en el primer plano, hasta que aparezca el menú con «Show All Windows». Pulsando este botón se abre la siguiente vista de las ventanas de aplicación, incluido un botón más (+) para crear una nueva ventana:
Con la aplicación en primer plano, deslízate hacia arriba para mostrar el dock nuevamente. Esta vez, arrastra el icono de la aplicación fuera del dock hasta que se convierta en una ventana flotante. Coloca la ventana en el lado derecho o izquierdo de la pantalla. Ahora tienes una segunda ventana ejecutándose en modo deslizante:
Con la ventana deslizante aún ejecutándose, toca y mantén presionado el controlador de arrastre en la parte superior de la ventana. Tira hacia abajo y hacia la derecha hasta que la ventana cambie de forma. Ahora tiene dos ventanas que se ejecutan una al lado de la otra. También puedes mover el controlador en el medio para cambiar el tamaño de estas ventanas:
Agregar soporte para escenas adicionales es bastante simple, pero debes considerar donde tiene sentido agregar este soporte y cómo mantener sus ventanas sincronizadas.
Actualización del contenido de las ventanas
Si has intentado implementar el manejo de las escenas, puedes notar que hay un problema con el estado de la interfaz cuando cambian los datos.
Se necesita una forma de decirle a la interfaz de usuario que actualice su estado actual y vuelva a pedir los datos. Aquí es donde entra en juego UISceneSession. Una sesión de escena puede estar en uno de los siguientes estados:
- Primer plano activo: la escena se ejecuta en primer plano y actualmente recibe eventos.
- Primer plano inactivo: la escena se está ejecutando en primer plano pero actualmente no recibe eventos.
- Fondo: la escena se ejecuta en segundo plano y no está en la pantalla.
- Desconectado: la escena no está conectada actualmente a la aplicación.
Las escenas pueden desconectarse en cualquier momento, porque iOS puede desconectarlas para liberar recursos.
Debes manejar las escenas tanto en primer plano como en segundo plano para mantener tus escenas actualizada .
Una manera de hacerlo es utilizar una herramienta familiar: NotificationCenter.
Puedes actualizar cualquier sesión en primer plano escuchando la notificación apropiada y solicitando actualizaciones.
Es posible actualizar las escenas que están en el segundo plano. Para encontrar y actualizar estas escenas, primero debes adjuntarles información de identificación. De esta forma, puedes encontrarlas luego. Para este propósito, se puede usar la propiedad userInfo de la sesión de escena.
Actualiza application(_:configurationForConnecting:options:) en AppDelegate.swift para adjuntar un diccionario userInfo a la sesión de escena. Justo después de crear la configuración de la escena, agrega el siguiente código:
let userInfo = [ "type": activity.rawValue ] connectingSceneSession.userInfo = userInfo
Luego puedes actualizar el estado de las escenas de siguiente manera:
func updateListViews() { let scenes = UIApplication.shared.connectedScenes let filteredScenes = scenes.filter { scene in guard let userInfo = scene.session.userInfo, let sceneType = userInfo["type"] as? String, sceneType == "specialViewId" else { return false } return true } filteredScenes.forEach { scene in UIApplication.shared.requestSceneSessionRefresh(scene.session) } }
Conclusiones
Agregar soporte para escenas cambia la forma en que la aplicación iOS responde a los eventos del ciclo de vida. En una aplicación sin escenas, el objeto delegado de la aplicación maneja las transiciones al primer plano o al fondo. Cuando añades el soporte de las escenas a tu aplicación, UIKit transfiere esa responsabilidad a sus objetos delegados de escena. Los ciclos de vida de la escena son independientes entre sí e independientes de la aplicación, por lo que los objetos delegados de la escena deben manejar las transiciones. Las escenas abren el nuevo camino hacia multitarea de otro nivel en iPadOS. Es una novedad muy importante no solo para los usuarios de iPad, pero también para los que tienen iPhone. En el futuro esta funcionalidad sin duda llegará a todos los dispositivos móviles de Apple.
Referencias
- https://www.raywenderlich.com/5814609-adopting-scenes-in-ipados
- https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports
- https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad