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

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):

MarcadorAppApp.swift: tal como lo genera Xcode (con comentarios)
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).

ejemplo mínimo: leer la fase (con comentarios)
// 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 active

Un 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
1
Ver solución
solución: cuándo deja de verse
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.

MarcadorApp (proyecto)
MarcadorApp
MarcadorApp.swiftse renombra desde MarcadorAppApp.swift y se modifica
Registro.swiftse crea
Un fichero se renombra y se modifica; otro se crea.

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:

  1. En el Project navigator, haz clic en MarcadorAppApp.swift para seleccionarlo, pulsa Return y escribe MarcadorApp.swift.
  2. Abre el fichero y cambia struct MarcadorAppApp: App por struct 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 Registro sin casos: un enum que no se puede instanciar y solo agrupa funciones (como el enum Ejemplo de ARC). Se usa como Registro.anotar("…").
  • Logger(subsystem:category:), del módulo os: 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
MarcadorApp/Registro.swift
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 -> active

Cuando termines, abre el bloque.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp.swift: con la fase de la escena
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, Ejemplo y Marcador no existen aquí.
  • Ejemplo.rayoContraToros: un partido de ejemplo del paquete. Es un Partido, con equipos (local, visitante) y sus eventos.
  • partido.marcador(): el Marcador del partido, que el paquete calcula; su description es el texto 2-1.
  • VStack(spacing: 12) apila sus hijos en vertical con 12 puntos entre ellos, y Text pinta 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()); .title2 es una más pequeña. .font, .bold() y .padding() son modificadores, como el .onChange de 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):

ejemplo mínimo: una vista con un VStack y dos Text
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 Toros

Cuando termines, abre el bloque.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
ContentView.swift: el partido de Torneo
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.

  1. Ejecuta la app con Cmd+R.
  2. En el simulador, sal al inicio con Device → Home (Shift+Cmd+H) y toca el icono de Ajustes.
  3. 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.Preferences abre Ajustes y xcrun simctl launch booted com.sebastiannarvaez.marcador vuelve a Marcador.
  4. Lee el registro desde una Terminal:
xcrun simctl spawn booted log show --last 2m --predicate 'subsystem == "com.sebastiannarvaez.marcador"' --style compact

booted 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 -> active

La 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 scenePhase de 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. 1.Creas un MarcadorApp.swift nuevo con su @main en lugar de renombrar el MarcadorAppApp.swift del asistente. ¿Qué ocurre?

  2. 2.Abres Ajustes desde el simulador estando en Marcador y vuelves. ¿Qué orden de fases registra scenePhase al salir?

  3. 3.SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor está en el target de la app. ¿Qué consecuencia tiene para tu código?

  4. 4.Registro.anotar escribe con print y también con Logger. ¿Para qué sirve el Logger?

  5. 5.Al instalar el paquete local Torneo, Xcode pregunta a qué target se añade. ¿Qué eliges?

Faltan 5 de 5