La app, sus escenas y el paquete Torneo
Al terminar: Entender App, Scene y scenePhase y enlazar el paquete Torneo a la app de iOS.
Al terminar tendrás en ~/Marcador-Swift/MarcadorApp una app de iOS que arranca en el simulador, enseña «Rayo FC 2-1 Toros» y apunta en un registro cada vez que su ventana cambia de fase. El resultado por pantalla es pequeño; lo importante es que el marcador sale del paquete Torneo que escribiste en la parte de Swift: la lógica se queda donde estaba y la app solo la usa.
Importa porque casi todo lo que hace una app (arrancar, ir al fondo, volver) pasa en el punto de entrada, y porque enlazar un paquete a un proyecto de Xcode es la receta que repetirás cada vez que una lección instale una librería.
Temas: @main, App, escena y scenePhase, el registro de la app (Registro), enlazar un paquete local y la primera pantalla con datos de Torneo.
Qué es un App, una escena y una fase
MarcadorAppApp.swift es el fichero que arranca la app. Así lo escribe el asistente (con sus comentarios quitados y otros, de explicación, puestos):
import SwiftUI
@main // marca el punto de entrada de la app
struct MarcadorAppApp: App { // una estructura que cumple el protocolo `App`
var body: some Scene { // `body` describe qué ventanas tiene la app
WindowGroup { // la ventana normal de una app de iPhone
ContentView() // la vista raíz de esa ventana
}
}
}body es de tipo some Scene: «alguna escena». some significa «un tipo concreto que no te molestas en escribir»; Swift lo deduce del contenido (a fondo, en SwiftUI: primeros pasos). Una escena (Scene) es un contenedor de vistas que el sistema gestiona.
Cada escena pasa por tres fases, que lees con @Environment(\.scenePhase). @Environment es una etiqueta que le pones a una propiedad para que SwiftUI la rellene con un dato del sistema; \.scenePhase es el nombre de ese dato (la barra invertida y el punto son la forma de escribir «la propiedad scenePhase del entorno») y vale la fase actual. Las etiquetas de este estilo se llaman property wrappers y las verás por separado desde @State.
El orden importa: al salir de la app se pasa por inactive antes de background; al volver, al revés (background, inactive, active).
// Una propiedad del App: el sistema la rellena con la fase actual de la escena.
@Environment(\.scenePhase) private var fase
// dentro de body: some Scene, tras WindowGroup { … }
.onChange(of: fase) { antigua, nueva in // corre cada vez que fase cambia; recibe la de antes y la de ahora
print("de \(antigua) a \(nueva)") // al abrir la app: de inactive a active
}Al abrir la app, la consola de Xcode escribe:
de inactive a activeUn modificador es una llamada que se escribe detrás de una vista (o de una escena) con un punto y que le cambia algo: .onChange(of:) le añade «haz esto cuando cambie tal cosa», .padding() le pone aire. Se explican a fondo en SwiftUI: primeros pasos.
En Android, .active se parece a onStart/onResume, .inactive a onPause y .background a onStop; es una simplificación, porque scenePhase habla de la ventana entera y no de una pantalla.
Reto · Ejercicio: cuándo deja de verse
En tu Playground, escribe un enum, Fase, con tres casos, activa, inactiva y segundoPlano, y una función, dejaDeSerVisible, que reciba la fase de la que se sale y a la que se entra, y diga si es el paso de inactiva a segundo plano. Guarda cada transición como una pareja (una tupla, la de Colecciones, diccionarios y propiedades) y recórrelas todas con un for. Al imprimir una fase, \(fase) escribe el nombre de su caso. Recorre estas cinco transiciones, en orden: de inactiva a activa, de activa a inactiva, de inactiva a segundo plano, de segundo plano a inactiva y de inactiva a activa. Imprime cada una que cumpla la condición y, al final, cuántas fueron. Lo que debe salir:
deja de ser visible: inactiva -> segundoPlano
1Ver solución
enum Fase { case activa, inactiva, segundoPlano }
func dejaDeSerVisible(desde antigua: Fase, hacia nueva: Fase) -> Bool {
antigua == .inactiva && nueva == .segundoPlano
}
let transiciones: [(Fase, Fase)] = [
(.inactiva, .activa), (.activa, .inactiva), (.inactiva, .segundoPlano),
(.segundoPlano, .inactiva), (.inactiva, .activa),
]
var cuantas = 0
for (antigua, nueva) in transiciones {
if dejaDeSerVisible(desde: antigua, hacia: nueva) {
cuantas += 1
print("deja de ser visible: \(antigua) -> \(nueva)")
}
}
print(cuantas)Solo la tercera transición cumple la condición. [(Fase, Fase)] es una lista de parejas y for (antigua, nueva) in transiciones separa cada pareja en sus dos valores. Un enum sin datos asociados se compara con == sin más código y se imprime con el nombre del caso.
Paso 1 · El punto de entrada y el registro
Objetivo: renombrar el fichero de entrada de la app, crear Registro y anotar cada cambio de fase de la ventana.
El asistente le pone «App» al final del nombre del proyecto y sale MarcadorAppApp. Aquí sobra: el módulo ya se llama MarcadorApp, y las lecciones siguientes dan por hecho ese nombre. Renómbralo:
- En el Project navigator, haz clic en
MarcadorAppApp.swiftpara seleccionarlo, pulsa Return y escribeMarcadorApp.swift. - Abre el fichero y cambia
struct MarcadorAppApp: Appporstruct MarcadorApp: App.
Crea Registro
Para ver las fases hace falta un sitio donde anotarlas. Registro será el único punto por el que pasen los mensajes de depuración de la app: el «Logcat» de iOS.
Crea el fichero: clic derecho sobre la carpeta MarcadorApp (la de dentro del proyecto, la que contiene MarcadorApp.swift, no el icono azul) → New File from Template…. En la ventana que se abre, pestaña iOS → Swift File → Next; escribe Registro, deja marcada en Targets la casilla MarcadorApp (la de la app, no las de pruebas) y pulsa Create. Se queda ahí, junto a MarcadorApp.swift, y no en una carpeta propia: es una herramienta de toda la app, no de una pantalla. Alternativa desde la Terminal: touch ~/Marcador-Swift/MarcadorApp/MarcadorApp/Registro.swift, y Xcode lo muestra solo.
Piezas nuevas de este fichero:
enum Registrosin casos: unenumque no se puede instanciar y solo agrupa funciones (como elenum Ejemplode ARC). Se usa comoRegistro.anotar("…").Logger(subsystem:category:), del móduloos: el registro del sistema. El subsystem es el nombre de tu app (aquí, su identificador) y sirve para filtrar tus mensajes entre los de todo el sistema.logger.notice("…")escribe una línea.privacy: .public: sin él, iOS sustituye el texto de las líneas por<private>cuando no hay un depurador delante (el depurador es la conexión de Xcode con la app mientras corre desde Xcode).
Intenta escribirlo tú: un enum sin casos, con una función anotar que reciba un texto y lo escriba en la consola de Xcode y en el registro del sistema. Cuando termines, abre el bloque.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import os // trae `Logger`, el registro del sistema
// Un único sitio por donde pasan los mensajes de depuración de la app.
enum Registro {
// El «subsystem» identifica tu app en el registro del sistema.
// Si usas otro identificador de paquete, cámbialo aquí también.
private static let logger = Logger(subsystem: "com.sebastiannarvaez.marcador", category: "app")
// `anotar` escribe el mensaje por dos caminos.
static func anotar(_ mensaje: String) {
print(mensaje) // a la consola de Xcode, mientras la app corre desde Xcode
fflush(stdout) // `print` junta el texto en una memoria intermedia (un búfer); esto la vacía para que la línea salga ya
// Al registro del sistema, que se puede leer después. `privacy: .public` evita el `<private>`.
logger.notice("\(mensaje, privacy: .public)")
}
}Anota las fases
Ahora MarcadorApp.swift lee la fase y la anota. Intenta escribirlo tú: una propiedad, fase, que lee del entorno la fase de la escena y, tras el cierre de WindowGroup, que cada vez que la fase cambie se anote con Registro la fase anterior y la nueva. Lo que debes ver en el registro al abrir la app, mandarla al fondo y volver:
scenePhase: inactive -> active
scenePhase: active -> inactive
scenePhase: inactive -> background
scenePhase: background -> inactive
scenePhase: inactive -> activeCuando termines, abre el bloque.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
@main
struct MarcadorApp: App {
// Lee la fase de la ESCENA: `.active` (delante y recibiendo toques), `.inactive`
// (visible pero sin interacción: una llamada, el selector de apps) o `.background`.
@Environment(\.scenePhase) private var fase
var body: some Scene {
WindowGroup {
ContentView()
}
// Se ejecuta cada vez que `fase` cambia. Con dos parámetros (antigua, nueva) es la forma de iOS 17 en adelante.
.onChange(of: fase) { antigua, nueva in
Registro.anotar("scenePhase: \(antigua) -> \(nueva)")
}
}
}Pulsa Cmd+B: debe terminar con Build Succeeded.
Paso 2 · Instala Torneo en la app
Objetivo: que import Torneo funcione dentro de la app, enlazando el paquete que ya tienes en ~/Marcador-Swift/Torneo.
Esta es la receta para instalar un paquete de Swift en una app. Cada vez que una lección necesite uno, la repetirá en corto.
1 · Qué instalas y por qué ahora. Torneo es un paquete local: vive en una carpeta de tu disco, no en internet. Xcode puede enlazarlo y compilarlo junto a la app, y los cambios que hagas en Torneo se ven en la app al recompilar. Hace falta ya porque la pantalla del paso siguiente saca su marcador de ahí.
2 · Añádelo. Con el proyecto abierto: File → Add Package Dependencies…. Se abre una ventana para buscar paquetes; abajo a la izquierda está el botón Add Local…. Púlsalo, elige la carpeta Torneo de ~/Marcador-Swift (la que contiene Package.swift) y pulsa Add Package.
3 · Qué producto y a qué target. Un paquete ofrece productos: lo que otros pueden usar (en Package.swift son las líneas de products). Torneo tiene dos: la librería Torneo, que es la que usarás, y el programa de consola torneo-cli. Xcode pregunta qué producto añadir y a qué target: marca Torneo y, en la columna Add to Target, MarcadorApp. No marques torneo-cli. Después Add Package.
Solo hace falta enlazarlo al target de la app: el target MarcadorAppTests corre dentro de la app y ve import Torneo sin enlazarlo aparte (lo comprobarás en la lección siguiente).
4 · Dónde se ve. Selecciona el icono azul del proyecto → pestaña Package Dependencies (junto a Build Settings): sale Torneo con su ruta local. Y en el target MarcadorApp → General → Frameworks, Libraries, and Embedded Content aparece el producto Torneo.
5 · Comprueba. Pulsa Cmd+B: debe terminar sin errores. Cuando compiles desde la Terminal con xcodebuild (lección siguiente), la salida empieza con Resolved source packages: y, debajo, Torneo: …/Marcador-Swift/Torneo @ local: así se ve que el paquete se ha resuelto desde tu carpeta.
Paso 3 · La pantalla con el partido de Torneo
Objetivo: sustituir el «Hello, world!» por el título «Marcador» y el texto «Rayo FC 2-1 Toros», sacado de Torneo. No hay que crear ningún fichero: ContentView.swift ya existe y se reescribe entero.
Piezas de este fichero:
import Torneo: trae el paquete. Sin esta línea,EjemployMarcadorno existen aquí.Ejemplo.rayoContraToros: un partido de ejemplo del paquete. Es unPartido, con equipos (local,visitante) y sus eventos.partido.marcador(): elMarcadordel partido, que el paquete calcula; sudescriptiones el texto2-1.VStack(spacing: 12)apila sus hijos en vertical con 12 puntos entre ellos, yTextpinta texto;.padding()añade aire alrededor. Se explican a fondo en SwiftUI: primeros pasos..font(.largeTitle.bold()): una tipografía grande (.largeTitle) en negrita (.bold());.title2es una más pequeña..font,.bold()y.padding()son modificadores, como el.onChangede antes.#Preview { … }: la vista previa de Xcode junto al código; puedes copiarla tal cual y no hace falta para ejecutar la app. Si quieres verla, Editor → Canvas abre el panel de la derecha (los menús pueden variar según tu versión de Xcode).
Las mismas piezas en otra pantalla, una ficha del tiempo (no es tu ContentView; solo para leerla):
import SwiftUI // trae View, Text, VStack…
struct FichaView: View { // una pantalla es un struct que cumple View
var body: some View { // body describe lo que se ve
let ciudad = "Cali" // un dato cualquiera, calculado aquí
let grados = 28
VStack(spacing: 8) { // apila en vertical, con 8 puntos entre hijos
Text("El tiempo")
.font(.largeTitle.bold()) // tipografía grande, en negrita
Text("\(ciudad): \(grados) °C") // un texto con datos interpolados
.font(.title2) // una tipografía algo más pequeña
}
.padding() // aire alrededor de todo el VStack
}
}
#Preview { // la vista previa del editor de Xcode
FichaView()
}Intenta escribirlo tú con esas piezas: con el partido de ejemplo del paquete, una columna con el título «Marcador» en tipografía grande y negrita y, debajo, más pequeño, un texto con el nombre del local, el marcador y el nombre del visitante, con aire alrededor. Lo que debes ver:
Marcador
Rayo FC 2-1 TorosCuando termines, abre el bloque.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
import Torneo // sin este import, `Ejemplo` y `Marcador` no existen en este fichero
struct ContentView: View {
var body: some View {
// Código del paquete: un partido de ejemplo.
let partido = Ejemplo.rayoContraToros
// Apila sus hijos en vertical, con 12 puntos entre ellos.
VStack(spacing: 12) {
Text("Marcador")
.font(.largeTitle.bold()) // tipografía grande y en negrita
// «Rayo FC 2-1 Toros»: el nombre del local, el marcador y el nombre del visitante.
Text("\(partido.local.nombre) \(partido.marcador().description) \(partido.visitante.nombre)")
.font(.title2)
}
.padding() // aire alrededor de todo el `VStack`
}
}
// La vista previa del editor de Xcode.
#Preview {
ContentView()
}Ejecuta con Cmd+R: debe verse «Marcador» y debajo «Rayo FC 2-1 Toros». El nombre bajo el icono, al salir al inicio del simulador, es «Marcador».
Paso 4 · Mira las fases en el registro
Objetivo: ver en el registro del sistema los cambios de fase que anota Registro.
- Ejecuta la app con Cmd+R.
- En el simulador, sal al inicio con Device → Home (Shift+Cmd+H) y toca el icono de Ajustes.
- Vuelve a Marcador: abre Device → App Switcher (el selector de apps) y toca la tarjeta de Marcador. Si ese menú no te sale, el plan B es la Terminal:
xcrun simctl launch booted com.apple.Preferencesabre Ajustes yxcrun simctl launch booted com.sebastiannarvaez.marcadorvuelve a Marcador. - Lee el registro desde una Terminal:
xcrun simctl spawn booted log show --last 2m --predicate 'subsystem == "com.sebastiannarvaez.marcador"' --style compactbooted es el simulador que está arrancado: tiene que estar arrancado ya, por eso se ejecuta tras Cmd+R. Verás algo como esto (la hora y los números entre corchetes cambian; lo que importa es el final de cada línea):
Timestamp Ty Process[PID:TID]
2026-09-30 23:21:30.726 Df MarcadorApp[24691:39d7cb] [com.sebastiannarvaez.marcador:app] scenePhase: inactive -> active
2026-09-30 23:21:34.852 Df MarcadorApp[24691:39d7cb] [com.sebastiannarvaez.marcador:app] scenePhase: active -> inactive
2026-09-30 23:21:36.655 Df MarcadorApp[24691:39d7cb] [com.sebastiannarvaez.marcador:app] scenePhase: inactive -> background
2026-09-30 23:21:39.236 Df MarcadorApp[24691:39d7cb] [com.sebastiannarvaez.marcador:app] scenePhase: background -> inactive
2026-09-30 23:21:39.676 Df MarcadorApp[24691:39d7cb] [com.sebastiannarvaez.marcador:app] scenePhase: inactive -> activeLa primera línea es el arranque de la app; las otras cuatro, el viaje a Ajustes y la vuelta. [com.sebastiannarvaez.marcador:app] es el subsystem y la category del Logger que escribiste.
Punto de control
- La app arranca en el simulador con el título «Marcador» y debajo «Rayo FC 2-1 Toros».
- El nombre bajo el icono pone «Marcador» y el registro muestra los cinco cambios de
scenePhasede más arriba. - En el proyecto, Package Dependencies lista
Torneo. - Tu Playground muestra el ejercicio: una sola transición, de inactiva a segundo plano, y el número 1.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Creas un
MarcadorApp.swiftnuevo con su@mainen lugar de renombrar elMarcadorAppApp.swiftdel asistente. ¿Qué ocurre?2.Abres Ajustes desde el simulador estando en Marcador y vuelves. ¿Qué orden de fases registra
scenePhaseal salir?3.
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActorestá en el target de la app. ¿Qué consecuencia tiene para tu código?4.
Registro.anotarescribe conprinty también conLogger. ¿Para qué sirve elLogger?5.Al instalar el paquete local
Torneo, Xcode pregunta a qué target se añade. ¿Qué eliges?