Saltar al contenido
K
iOS con SwiftUIPaso a paso28 min de lectura · 8 min de máquina Básico

Un UIViewController en SwiftUI

Al terminar: Ver cada callback de un UIViewController y compararlo con el de la Activity.

Al terminar, Marcador tendrá un botón «Ciclo de vida (UIKit)» que abre una hoja con un UIViewController de verdad, una pantalla escrita con UIKit, el sistema de interfaces de iOS anterior a SwiftUI. Al abrirla, el registro mostrará su primer aviso, viewDidLoad. Los demás avisos llegan en la lección siguiente, El ciclo de vida de UIViewController.

Importa porque SwiftUI, la forma actual de escribir pantallas, se apoya en UIKit por debajo, y todavía hay código y librerías que entregan un UIViewController. Y es el mismo experimento que hiciste con la Activity de Android, ahora en el otro sistema.

Un UIViewController es una pantalla que crea el sistema

UIKit es el sistema de interfaces de iOS anterior a SwiftUI. En UIKit, cada pantalla es un UIViewController: una clase que gestiona una jerarquía de vistas (el árbol de elementos visibles, de la vista raíz a cada etiqueta).

Un UIViewController no se construye desde cero: se hereda. Los métodos que el sistema llama sobre tu pantalla (los avisos) los recibes redefiniéndolos, y para eso hace falta entender la herencia.

Herencia: una clase que parte de otra

Herencia es que una clase parte de otra y recibe todo lo suyo: class Mia: UIViewController dice «Mia es un UIViewController con lo que yo añada». La clase de la que se parte es la clase madre (superclass); la nueva, la subclase.

  • override: «sustituyo un método de la clase madre por el mío». Swift exige escribirlo: si redefines un método heredado sin override, el compilador se queja.
  • super: la clase madre, para llamar a su versión: super.viewDidLoad() ejecuta lo que ya hacía UIViewController antes de tu código. Si no la llamas, la clase madre no hace su parte.
  • final class: una clase que no admite subclases. En Swift las clases admiten subclases salvo que las marques con final.

Los métodos de UIKit son un caso claro: tú no llamas a viewDidAppear; el sistema lo llama sobre tu subclase, y por eso escribes override y llamas a super primero.

Las tres piezas, con una tienda en vez de una pantalla (puedes probarlo en tu Playground):

ejemplo mínimo: una clase y su subclase (con comentarios)
// Una tienda: la clase madre. Tiene un método, `abrir`.
class Tienda {
    func abrir() {
        print("abriendo la tienda")
    }
}
 
// Una tienda con ofertas: hereda de `Tienda` y cambia lo que hace `abrir`.
// `final`: nadie podrá heredar de ella.
final class TiendaConOfertas: Tienda {
    private var diasAbierta = 0          // cuántos días ha abierto
 
    override func abrir() {              // `override`: sustituyo el `abrir` de la clase madre
        super.abrir()                    // primero, lo que ya hacía `Tienda`
        diasAbierta += 1                 // después, lo mío: sumo un día
        print("día \(diasAbierta): hoy hay ofertas")
    }
}
 
let tienda = TiendaConOfertas()          // una tienda con ofertas
tienda.abrir()                           // la primera vez
tienda.abrir()                           // la segunda

Al ejecutarlo sale:

abriendo la tienda
día 1: hoy hay ofertas
abriendo la tienda
día 2: hoy hay ofertas

Si quitas override, el compilador dice error: overriding declaration requires an 'override' keyword. Si quitas la línea de super.abrir(), desaparece «abriendo la tienda»: la clase madre no hace su parte.

Reto · Ejercicio: una pantalla que avisa antes y después

En tu Playground (reutiliza el mismo y borra lo anterior), escribe una clase, Pantalla, con un método, alAparecer, que imprima «apareciendo», y una subclase, PantallaConAviso, que lo redefina para que, alrededor de lo que hace la clase madre, imprima «preparando» antes y «lista» después. Crea una pantalla con aviso y llama a alAparecer una vez. Lo que debe salir:

preparando
apareciendo
lista

Después quita la palabra que dice «redefino» y lee el error.

Ver solución
solución: una pantalla que avisa antes y después
class Pantalla {
    func alAparecer() {
        print("apareciendo")
    }
}
 
final class PantallaConAviso: Pantalla {
    override func alAparecer() {
        print("preparando")      // lo mío, antes
        super.alAparecer()       // lo de la clase madre, en medio
        print("lista")           // lo mío, después
    }
}
 
PantallaConAviso().alAparecer()

super se puede llamar donde haga falta, no solo la primera: aquí queda en medio. Sin override, el compilador rechaza la subclase con error: overriding declaration requires an 'override' keyword: redefine un método heredado sin decirlo. Sin la línea de super, desaparece «apareciendo»: la clase madre no hace su parte.

Trabajas en ~/Marcador-Swift/MarcadorApp, el proyecto de la lección anterior. Los ficheros viven en la carpeta MarcadorApp del Project navigator (Cmd+1), la que contiene MarcadorApp.swift. Para crear cada fichero nuevo: clic derecho sobre la carpeta que te indique el paso → New File from Template… → Swift File → Next, el nombre y Create. Si el menú de tu versión de Xcode se llama distinto, usa la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorApp/<carpeta> y después touch ~/Marcador-Swift/MarcadorApp/MarcadorApp/<carpeta>/Nombre.swift, y Xcode lo muestra solo.

Paso 1 · Un controlador en su propio fichero

Objetivo: un UIViewController que compila, pinta su fondo, centra una etiqueta de texto y anota en el registro que su vista se ha cargado. Todavía no se ve en ninguna parte.

MarcadorApp (proyecto)
MarcadorApp
Laboratorio
CicloDeVidaViewController.swiftse crea
En el Project navigator: un fichero nuevo.

CicloDeVidaViewController.swift contendrá una pantalla escrita con UIKit. Va en una carpeta nueva, Laboratorio: clic derecho sobre la carpeta MarcadorApp (la que contiene MarcadorApp.swift, no el icono azul) → New Folder: aparece una carpeta con el nombre editable; escribe Laboratorio y pulsa Return. Es una carpeta real de tu disco y entra sola en la app. Después, clic derecho sobre Laboratorio → New File from Template… → Swift File → Next, escribe CicloDeVidaViewController y pulsa Create. Créalo ahora, aunque esté vacío; en seguida lo rellenas. Desde la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorApp/Laboratorio && touch ~/Marcador-Swift/MarcadorApp/MarcadorApp/Laboratorio/CicloDeVidaViewController.swift.

La clase mínima

Intenta escribir en CicloDeVidaViewController.swift una clase con un solo aviso, viewDidLoad, que el sistema llama una vez, cuando la vista del controlador ya existe, y que anota una línea en el registro. Cuando termines, abre el bloque:

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Laboratorio/CicloDeVidaViewController.swift
import UIKit   // UIKit, el sistema de pantallas anterior a SwiftUI
 
// `final class` que hereda de `UIViewController`: una pantalla de UIKit.
final class CicloDeVidaViewController: UIViewController {
 
    // El sistema llama a este método una vez, cuando la vista del controlador se ha creado.
    override func viewDidLoad() {
        super.viewDidLoad()   // primero, lo que ya hacía la clase madre
        Registro.anotar("Ciclo CicloDeVidaViewController viewDidLoad")   // una línea en el registro
        // `view` es la vista raíz del controlador. `.systemBackground` es el color de fondo
        // del sistema: blanco en modo claro y negro en modo oscuro.
        view.backgroundColor = .systemBackground
    }
}

Pulsa Cmd+B: debe terminar con Build Succeeded.

Una etiqueta, centrada con restricciones

Para que la hoja no esté vacía, el controlador pinta una etiqueta de texto (UILabel) en el centro. Es el único código de UIKit de esta lección que te damos hecho, porque necesita restricciones (Auto Layout): reglas que colocan una vista, del estilo «mi centro horizontal es el de mi padre», y que no son el tema de aquí. Añade estas líneas al final de viewDidLoad, tras view.backgroundColor = .systemBackground:

viewDidLoad: la etiqueta, al final
        let etiqueta = UILabel()                                        // una etiqueta de texto
        etiqueta.text = "Un UIViewController\n(mira el registro)"      // el texto; `\n` es un salto de línea
        etiqueta.numberOfLines = 0                                     // tantas líneas como haga falta
        etiqueta.textAlignment = .center                               // texto centrado
        etiqueta.font = .preferredFont(forTextStyle: .title2)          // tipografía del sistema
        etiqueta.accessibilityIdentifier = "etiquetaDelControlador"    // un nombre interno: las pruebas la encuentran por él
        etiqueta.translatesAutoresizingMaskIntoConstraints = false     // «la colocan mis restricciones, no el sistema antiguo»
        view.addSubview(etiqueta)                                      // la mete dentro de la vista raíz
        NSLayoutConstraint.activate([                                  // activa las reglas de colocación
            etiqueta.centerXAnchor.constraint(equalTo: view.centerXAnchor),   // centrada en horizontal
            etiqueta.centerYAnchor.constraint(equalTo: view.centerYAnchor),   // y en vertical
            etiqueta.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 24),   // al menos 24 puntos de margen a la izquierda
        ])

Cmd+B otra vez: Build Succeeded.

Paso 2 · Ábrelo desde SwiftUI

Objetivo: un botón «Ciclo de vida (UIKit)» en la pantalla de Marcador que abre una hoja con el controlador. Al abrirla, el registro muestra viewDidLoad.

MarcadorApp (proyecto)
MarcadorApp
Laboratorio
CicloDeVidaViewController.swiftse modifica: el puente
ContentView.swiftse modifica: botón y hoja
Un fichero se modifica (el puente va al final del que ya tienes) y otro también.

Para abrir el controlador vas a usar unas piezas de SwiftUI que verás a fondo en las lecciones siguientes; aquí cópialas tal cual y fíjate solo en qué hace cada una:

  • Una hoja (.sheet): una pantalla modal (que se pone encima de la actual y pide que la cierres para seguir) que sube desde abajo y se cierra arrastrándola.
  • @State: un dato que cambia y que SwiftUI conserva; aquí, «¿está abierta la hoja?». Cuando cambia, SwiftUI vuelve a dibujar la pantalla. Se explica en @State.
  • $ delante del nombre ($mostrandoControlador): entrega al .sheet una conexión de lectura y escritura a ese dato, porque la hoja también escribe false cuando la cierras arrastrando. Se explica en @Binding.
  • Button("…") { … }: un botón; lo que va entre llaves es lo que hace al tocarlo. .buttonStyle(.borderedProminent) lo pinta relleno.

Falta una pieza: SwiftUI sabe mostrar vistas de SwiftUI, no controladores de UIKit. UIViewControllerRepresentable es un protocolo que mete un controlador de UIKit dentro de una vista de SwiftUI: makeUIViewController se llama una vez, y updateUIViewController cada vez que cambian los datos de la vista. El parámetro context son datos que SwiftUI le pasa (y que aquí no se usan); no tiene nada que ver con el Context de Android. Es de SwiftUI, así que el fichero necesita un segundo import: añade import SwiftUI encima de import UIKit. Después, añade el puente al final de CicloDeVidaViewController.swift, fuera de la clase:

CicloDeVidaViewController.swift: el puente, al final del fichero
// Una vista de SwiftUI que contiene el controlador de UIKit.
struct CicloDeVidaView: UIViewControllerRepresentable {
    // SwiftUI la llama una vez para crear el controlador. `context` son datos de SwiftUI; aquí no se usa.
    func makeUIViewController(context: Context) -> CicloDeVidaViewController {
        CicloDeVidaViewController()
    }
 
    // SwiftUI la llama cada vez que cambian los datos de la vista. Aquí no hay nada que actualizar.
    func updateUIViewController(_ controlador: CicloDeVidaViewController, context: Context) {}
}

Si olvidas el import SwiftUI, Xcode marca el puente con error: cannot find type 'UIViewControllerRepresentable' in scope.

Y ContentView.swift gana un estado, un botón y la hoja. Como las piezas son de SwiftUI y todavía no las has estudiado, no hace falta que lo escribas de memoria: abre el bloque y sustituye el fichero entero por este. Al tocar el botón sube una hoja con «Un UIViewController (mira el registro)», y se cierra arrastrándola hacia abajo.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
ContentView.swift: con el botón y la hoja
import SwiftUI
import Torneo
 
struct ContentView: View {
    // Guarda si la hoja está abierta (pieza de SwiftUI que verás más adelante). `private`: solo esta vista lo usa.
    @State private var mostrandoControlador = false
 
    var body: some View {
        let partido = Ejemplo.rayoContraToros
        VStack(spacing: 12) {
            Text("Marcador")
                .font(.largeTitle.bold())
            Text("\(partido.local.nombre) \(partido.marcador().description) \(partido.visitante.nombre)")
                .font(.title2)
            // Un botón con acción (al tocarlo, abre la hoja) y estilo destacado.
            Button("Ciclo de vida (UIKit)") { mostrandoControlador = true }
                .buttonStyle(.borderedProminent)
        }
        .padding()
        // La hoja: se muestra mientras `mostrandoControlador` sea `true`. `$` es la conexión de lectura y escritura.
        .sheet(isPresented: $mostrandoControlador) {
            CicloDeVidaView()
        }
    }
}
 
#Preview {
    ContentView()
}

Ejecuta con Cmd+R, toca el botón y comprueba que sube una hoja con «Un UIViewController (mira el registro)». Ciérrala arrastrándola hacia abajo. En la consola de Xcode (View → Debug Area → Activate Console, Cmd+Shift+Y) sale una sola línea de tu controlador, la del Registro que escribiste en viewDidLoad:

Ciclo CicloDeVidaViewController viewDidLoad

Faltan los demás avisos: son el tema de la lección siguiente.

Punto de control

  • Desde ~/Marcador-Swift/MarcadorApp, xcodebuild -scheme MarcadorApp -destination 'platform=iOS Simulator,name=iPhone 17' build test termina con ** TEST SUCCEEDED ** (con el simulador que tengas: cambia iPhone 17 por un nombre de xcrun simctl list devices available): tres pruebas, una unitaria y dos de interfaz, las mismas de antes.
  • Al tocar «Ciclo de vida (UIKit)» sube una hoja con «Un UIViewController (mira el registro)», y se cierra arrastrándola hacia abajo.
  • La consola muestra Ciclo CicloDeVidaViewController viewDidLoad al abrir la hoja.
  • CicloDeVidaViewController.swift tiene la clase CicloDeVidaViewController y, al final, el puente CicloDeVidaView.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Quién llama a viewDidLoad de tu UIViewController?

  2. 2.¿Qué hace super.viewDidLoad() al principio de tu viewDidLoad?

  3. 3.Redefines un método heredado sin escribir override. ¿Qué pasa?

  4. 4.¿Para qué sirve UIViewControllerRepresentable?

  5. 5.En un override, ¿tiene que ir super siempre en la primera línea?

Faltan 5 de 5