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 sinoverride, el compilador se queja.super: la clase madre, para llamar a su versión:super.viewDidLoad()ejecuta lo que ya hacíaUIViewControllerantes 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 confinal.
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):
// 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 segundaAl ejecutarlo sale:
abriendo la tienda
día 1: hoy hay ofertas
abriendo la tienda
día 2: hoy hay ofertasSi 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
listaDespués quita la palabra que dice «redefino» y lee el error.
Ver solución
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.
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
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:
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.
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.sheetuna conexión de lectura y escritura a ese dato, porque la hoja también escribefalsecuando 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:
// 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
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 viewDidLoadFaltan 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 testtermina con** TEST SUCCEEDED **(con el simulador que tengas: cambiaiPhone 17por un nombre dexcrun 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 viewDidLoadal abrir la hoja. CicloDeVidaViewController.swifttiene la claseCicloDeVidaViewControllery, al final, el puenteCicloDeVidaView.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Quién llama a
viewDidLoadde tuUIViewController?2.¿Qué hace
super.viewDidLoad()al principio de tuviewDidLoad?3.Redefines un método heredado sin escribir
override. ¿Qué pasa?4.¿Para qué sirve
UIViewControllerRepresentable?5.En un
override, ¿tiene que irsupersiempre en la primera línea?