Programación en Swift y SwiftUI para iOS Developers

Como integrar SwiftUI en UIKit

El ecosistema de desarrollo de Apple ha experimentado una revolución masiva en los últimos años. Como iOS Developer, es muy probable que te hayas encontrado en la encrucijada entre mantener una base de código heredada (legacy) y el deseo de adoptar las nuevas tecnologías que Apple presenta cada año. El salto en la programación Swift nunca ha sido tan emocionante como ahora.

SwiftUI ha cambiado la forma en que construimos interfaces de usuario, ofreciendo una sintaxis declarativa que reduce drásticamente las líneas de código y los errores comunes. Sin embargo, reescribir una aplicación completa desde cero rara vez es una opción viable para las empresas. Aquí es donde surge una de las habilidades más valiosas en la actualidad: saber exactamente como integrar SwiftUI en UIKit.

En este extenso tutorial, exploraremos a fondo cómo puedes combinar lo mejor de ambos mundos. Utilizando Swift y Xcode, aprenderemos a inyectar vistas modernas en tus aplicaciones existentes, no solo en iOS, sino también expandiendo nuestros horizontes hacia macOS y watchOS.


1. El Puente entre Dos Mundos: La Familia HostingController

El secreto fundamental para incrustar SwiftUI en los frameworks imperativos clásicos (UIKit, AppKit y WatchKit) reside en una familia de controladores especiales proporcionados por Apple. Estos controladores actúan como envoltorios (wrappers) nativos que traducen la jerarquía de vistas declarativas de SwiftUI en algo que el sistema clásico puede entender y renderizar.

Dependiendo de la plataforma en la que estés trabajando dentro de Xcode, utilizarás un controlador diferente:

  • iOS / tvOS: UIHostingController (hereda de UIViewController).
  • macOS: NSHostingController (hereda de NSViewController).
  • watchOS: WKHostingController (hereda de WKInterfaceController).

Al heredar de las clases base estándar de cada plataforma, estos controladores de alojamiento pueden ser introducidos en tu jerarquía de navegación existente (como un UINavigationController o presentar un modal) exactamente igual que cualquier otro controlador de tu aplicación.


2. Integración en iOS: Inyectando SwiftUI en UIKit

Vamos a sumergirnos en el código. Imagina que tienes una aplicación tradicional en iOS y tu equipo de diseño ha creado una nueva y espectacular pantalla de perfil de usuario. En lugar de lidiar con Auto Layout y UITableView, decides construirla en SwiftUI.

Paso 2.1: Creando la Vista en SwiftUI

Primero, creamos nuestra vista declarativa en Swift. Abre tu proyecto en Xcode y crea un nuevo archivo SwiftUI View.

import SwiftUI

struct UserProfileView: View {
    var username: String
    var bio: String
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.circle.fill")
                .resizable()
                .frame(width: 100, height: 100)
                .foregroundColor(.blue)
            
            Text(username)
                .font(.largeTitle)
                .fontWeight(.bold)
            
            Text(bio)
                .font(.body)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
                .padding()
            
            Button(action: {
                print("Perfil editado")
            }) {
                Text("Editar Perfil")
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding(.horizontal)
            
            Spacer()
        }
        .padding()
    }
}

Paso 2.2: Presentando mediante Código (Programmatically)

Si tu arquitectura en programación Swift se basa en inicializar vistas por código (sin Storyboards), el proceso es extremadamente directo. Necesitamos instanciar un UIHostingController y pasarle nuestra UserProfileView como la vista raíz.

Desde cualquier UIViewController existente, puedes hacer lo siguiente:

import UIKit
import SwiftUI

class HomeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupNavigateButton()
    }
    
    private func setupNavigateButton() {
        let button = UIButton(type: .system)
        button.setTitle("Ver Perfil (SwiftUI)", for: .normal)
        button.addTarget(self, action: #selector(showProfile), for: .touchUpInside)
        
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    @objc private func showProfile() {
        // 1. Instanciamos la vista de SwiftUI
        let swiftUIView = UserProfileView(username: "AppleFan99", bio: "Apasionado iOS Developer creando el futuro.")
        
        // 2. Envolvemos la vista en un UIHostingController
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // 3. Presentamos el controlador como lo haríamos con cualquier UIViewController
        navigationController?.pushViewController(hostingController, animated: true)
        
        // Alternativamente, para presentar de forma modal:
        // present(hostingController, animated: true)
    }
}

Paso 2.3: Integración mediante Storyboards / Interface Builder

Si tu proyecto actual depende en gran medida de Storyboards, Xcode te facilita enormemente el trabajo. Apple introdujo los Hosting Controller directamente en la biblioteca de objetos (Object Library) de Interface Builder.

  1. Abre tu archivo Main.storyboard.
  2. Presiona Cmd + Shift + L para abrir la librería.
  3. Busca Hosting Controller y arrástralo a tu lienzo.
  4. Crea un Segue desde tu controlador de origen hacia este nuevo Hosting Controller.
  5. Para inyectar la vista, usaremos un IBSegueAction.

En tu UIViewController de origen, añade el siguiente código:

import UIKit
import SwiftUI

class DashboardViewController: UIViewController {

    // Esta función se enlaza directamente desde el Storyboard
    @IBSegueAction func showSwiftUIProfile(_ coder: NSCoder) -> UIViewController? {
        let profileView = UserProfileView(username: "StoryboardUser", bio: "Integrando SwiftUI visualmente.")
        return UIHostingController(coder: coder, rootView: profileView)
    }
}

Nota: Arrastra desde el segue en el Storyboard hacia el código de tu controlador para conectar esta acción.


3. Comunicación Bidireccional: Datos y Delegados

Saber como integrar SwiftUI en UIKit es solo el primer paso. El verdadero desafío para un iOS Developer es lograr que ambas tecnologías se comuniquen fluidamente. ¿Qué sucede si un botón en SwiftUI necesita actualizar una etiqueta en UIKit, o si UIKit descarga datos de internet que SwiftUI necesita mostrar?

De UIKit a SwiftUI: ObservableObject

Para pasar datos dinámicos desde el mundo imperativo al declarativo, la mejor herramienta en Swift es el protocolo ObservableObject junto con la librería Combine.

Creamos un modelo de vista (ViewModel):

import Foundation
import Combine

class UserViewModel: ObservableObject {
    @Published var followerCount: Int = 0
    
    func fetchFollowers() {
        // Simulamos una llamada de red
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.followerCount = Int.random(in: 100...5000)
        }
    }
}

Modificamos nuestra vista SwiftUI para que reaccione a este modelo:

import SwiftUI

struct StatsView: View {
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        VStack {
            Text("Seguidores")
                .font(.headline)
            Text("\(viewModel.followerCount)")
                .font(.system(size: 50, weight: .bold))
                .foregroundColor(.green)
        }
    }
}

En nuestro controlador de UIKit, mantenemos la referencia del ViewModel y actualizamos los datos cuando sea necesario. SwiftUI se redibujará automáticamente.

class StatsViewController: UIViewController {
    var viewModel = UserViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let swiftUIView = StatsView(viewModel: viewModel)
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // Añadir el hosting controller como hijo
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostingController.didMove(toParent: self)
        
        // Iniciar la descarga de datos desde UIKit
        viewModel.fetchFollowers()
    }
}

De SwiftUI a UIKit: Closures y Delegados

Si necesitas notificar a UIKit sobre una acción del usuario en SwiftUI (por ejemplo, tocar un botón de “Cerrar” o “Guardar”), los closures (bloques de código) son la forma más “Swift-friendly” de lograrlo.

import SwiftUI

struct ActionView: View {
    // Closure que será implementado por UIKit
    var onDismiss: (() -> Void)?
    
    var body: some View {
        Button(action: {
            // Ejecutamos el closure cuando se toca el botón
            onDismiss?()
        }) {
            Text("Cerrar Pantalla")
                .foregroundColor(.red)
        }
    }
}

En UIKit:

class PresenterViewController: UIViewController {
    
    func presentActionView() {
        var actionView = ActionView()
        
        // Definimos el comportamiento del closure
        actionView.onDismiss = { [weak self] in
            self?.dismiss(animated: true, completion: nil)
        }
        
        let hostingController = UIHostingController(rootView: actionView)
        present(hostingController, animated: true)
    }
}

4. Expandiendo Horizontes: Integración en macOS (AppKit)

El poder de la programación Swift moderna es que el paradigma declarativo es multiplataforma. Si estás desarrollando para computadoras Mac usando Xcode, el framework tradicional no es UIKit, sino AppKit.

Para integrar SwiftUI en una aplicación heredada de macOS, el proceso es conceptualmente idéntico al de iOS, pero usamos NSHostingController o NSHostingView.

Usando NSHostingView

A diferencia de iOS, en AppKit es muy común trabajar directamente a nivel de vista (View) en lugar de depender únicamente de controladores de vista (View Controllers). NSHostingView nos permite incrustar SwiftUI directamente en una jerarquía de NSView.

import Cocoa
import SwiftUI

class MacSettingsViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. Creamos la vista SwiftUI
        let settingsSwiftUIView = Text("Preferencias del Sistema (SwiftUI)")
            .font(.title)
            .padding()
        
        // 2. Creamos el NSHostingView
        let hostingView = NSHostingView(rootView: settingsSwiftUIView)
        hostingView.translatesAutoresizingMaskIntoConstraints = false
        
        // 3. Lo añadimos a la vista principal de AppKit
        self.view.addSubview(hostingView)
        
        // 4. Configuramos los constraints
        NSLayoutConstraint.activate([
            hostingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            hostingView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

Usando NSHostingController

Si prefieres trabajar a nivel de controlador de ventana/vista en macOS:

import AppKit
import SwiftUI

func openSwiftUIWindow() {
    let mySwiftUIView = UserProfileView(username: "MacUser", bio: "Desarrollo en macOS")
    let hostingController = NSHostingController(rootView: mySwiftUIView)
    
    let window = NSWindow(contentViewController: hostingController)
    window.title = "Ventana SwiftUI en AppKit"
    window.makeKeyAndOrderFront(nil)
}

5. El Reloj Inteligente: Integración en watchOS (WatchKit)

El desarrollo en watchOS fue uno de los primeros en adoptar SwiftUI de forma nativa. De hecho, Apple recomienda encarecidamente que todas las nuevas aplicaciones de watchOS se construyan puramente con este framework declarativo. Sin embargo, si como iOS Developer debes mantener una aplicación antigua que usa Storyboards de WatchKit (WKInterfaceController), también existe un puente.

Utilizamos WKHostingController. Funciona de manera un poco diferente a sus contrapartes en iOS y macOS, ya que los controladores de WatchKit no tienen las mismas jerarquías de vistas flexibles.

import WatchKit
import Foundation
import SwiftUI

// Tu vista moderna en SwiftUI
struct HeartRateView: View {
    var bpm: Int
    
    var body: some View {
        VStack {
            Image(systemName: "heart.fill")
                .foregroundColor(.red)
                .font(.largeTitle)
            Text("\(bpm) BPM")
                .font(.title2)
        }
    }
}

// El controlador de WatchKit clásico que aloja la vista
class HeartRateHostingController: WKHostingController<HeartRateView> {
    
    // Debes sobreescribir esta propiedad computada para devolver tu vista SwiftUI
    override var body: HeartRateView {
        return HeartRateView(bpm: 72)
    }
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        // Configuración inicial adicional
    }
}

En WatchKit, necesitas especificar el tipo de la vista genérica (<HeartRateView>) en la declaración de la clase y proveer la instancia en la propiedad body.


6. Mejores Prácticas y Rendimiento para el iOS Developer

Aprender la sintaxis de como integrar SwiftUI en UIKit es relativamente rápido, pero dominar la arquitectura requiere experiencia. Aquí hay varias recomendaciones clave al combinar estos frameworks en Xcode:

Gestión del Ciclo de Vida (Lifecycle)

Las vistas declarativas no tienen un ciclo de vida exacto como viewDidLoad o viewWillAppear. Tienen modificadores como .onAppear y .onDisappear.

  • Problema común: Si inicias llamadas de red pesadas en .onAppear dentro de una celda de una lista mixta de UIKit/SwiftUI, podrías saturar el hilo principal porque .onAppear puede llamarse múltiples veces durante el scroll.
  • Solución: Coordina las llamadas de red desde tu UIViewController padre y simplemente inyecta los datos resultantes a través de un @ObservedObject o mediante un estado que se pase a la vista incrustada.

Tamaño Intrínseco (Intrinsic Content Size)

Cuando añades un UIHostingController dentro de un diseño basado en Auto Layout, a menudo quieres que la vista de UIKit se adapte al tamaño exacto del contenido de tu vista declarativa.
A partir de iOS 16, Apple mejoró la propiedad sizingOptions en el controlador de alojamiento.

let hostingController = UIHostingController(rootView: myView)
// Permite que la vista UIKit se dimensione automáticamente según el contenido SwiftUI
hostingController.sizingOptions = .intrinsicContentSize

Limitar los Puntos de Cruce (Bridge Points)

Aunque es técnicamente posible tener un UIViewController que contenga un UIHostingController, que a su vez tenga un UIViewRepresentable para mostrar un UILabel de UIKit clásico, no lo hagas.
El cambio de contexto entre el sistema de renderizado de Core Animation imperativo y el motor de estado declarativo tiene un pequeño coste de rendimiento.

  • Regla de oro: Intenta que el “puente” ocurra solo a nivel de “Pantalla Completa” (Screen-level) o de “Componente Completo” (como una celda de tabla compleja completa). Evita incrustaciones profundas y anidadas como si fueran muñecas rusas.

Cuidado con los Retain Cycles en Closures

Como vimos en la sección de comunicación bidireccional, al pasar closures desde tu UIViewController hacia tus vistas SwiftUI, asegúrate siempre de capturar [weak self] o [unowned self] si el closure hace referencia a propiedades del controlador padre. Dado que el controlador padre retiene al UIHostingController, el cual retiene a la vista SwiftUI (donde vive el closure), es muy fácil crear un ciclo de retención (Memory Leak) en la programación Swift.


Resumen Final

El camino del iOS Developer moderno requiere adaptabilidad. Saber como integrar SwiftUI en UIKit (y sus contrapartes AppKit y WatchKit) te otorga un superpoder: la capacidad de modernizar aplicaciones iterativamente, una pantalla a la vez, sin el riesgo y el costo de reescribir un proyecto entero desde cero.

Xcode ha madurado lo suficiente como para que esta convivencia sea robusta y eficiente. Hemos visto que la llave maestra para este flujo de trabajo en la programación Swift son los controladores de alojamiento (UIHostingController, NSHostingController y WKHostingController). Combinando estas herramientas con un manejo adecuado del flujo de datos usando ObservableObject y Combine, puedes crear experiencias de usuario impecables e híbridas en todo el ecosistema de Swift.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Como integrar AppKit con SwiftUI

Related Posts