Programación en Swift y SwiftUI para iOS Developers

Cómo depurar y hacer debug en SwiftUI

Crear interfaces de usuario con SwiftUI se siente, a menudo, como magia. Escribes unas pocas líneas de código declarativo y voilà, tienes una interfaz funcional en iOS, macOS y watchOS. Pero, ¿qué sucede cuando la magia falla? ¿Qué pasa cuando tu vista no se actualiza, la animación da tirones o los datos simplemente no aparecen?

A diferencia de UIKit, donde podíamos seguir el flujo imperativo paso a paso, SwiftUI es un sistema reactivo basado en estados. Debuggear aquí requiere un cambio de mentalidad: no buscas solo “dónde falló el código”, sino “por qué el estado no es el que espero”.

En este tutorial de 2000 palabras, vamos a diseccionar las herramientas, técnicas y secretos para hacer debug en Xcode como un ingeniero senior, cubriendo desde lo básico hasta trucos avanzados de LLDB e Instruments.


1. El Cambio de Paradigma: Debuggear Estado vs. Flujo

Antes de tocar una sola tecla, debes entender el campo de batalla. En la programación imperativa (UIKit/AppKit), el bug suele estar en la secuencia de ejecución: “Llamé a la función A, pero no ejecutó la función B”.

En SwiftUI, el bug casi siempre es una desincronización de estado.

  • La Vista es una función del Estado: View=f(State).
  • Si la UI está mal, es porque el StateBinding o Environment es incorrecto.

Tu objetivo al debuggear no es solo ver la jerarquía de vistas, sino inspeccionar la “verdad” de tus datos en un momento específico del tiempo.


2. Xcode Previews: Tu Primera Línea de Defensa

Muchos desarrolladores cometen el error de ignorar las Previews cuando las cosas se ponen difíciles y corren directamente al simulador. Sin embargo, las Previews son un entorno de debug aislado increíblemente potente.

Debugging de Diseño Multi-Plataforma

Cuando desarrollas para iOS, macOS y watchOS simultáneamente, el layout es tu primer enemigo. En lugar de ejecutar tres simuladores, utiliza las previews para visualizar los estados límite.

struct MiVista_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MiVista(vm: ViewModel(estado: .cargando))
                .previewDisplayName("iOS Cargando")
            
            MiVista(vm: ViewModel(estado: .error))
                .previewDevice("Apple Watch Series 9 (45mm)")
                .previewDisplayName("WatchOS Error")
            
            MiVista(vm: ViewModel(estado: .exito))
                .previewDevice("Mac")
                .previewDisplayName("MacOS Éxito")
        }
    }
}

El “Selectable Mode”

En la parte inferior izquierda del canvas de Xcode, tienes dos botones vitales: “Live” (el icono de play) y “Selectable” (el icono de cursor).

  • Live Mode: Para probar interacciones y animaciones.
  • Selectable Mode: Al hacer clic en un elemento de la preview, Xcode resalta la línea exacta de código que generó esa vista. Si tienes un HStack anidado dentro de un VStack y no sabes por qué hay un padding extra, este modo es la forma más rápida de encontrar al culpable.

3. Inspección Visual: View Hierarchy Debugger

A veces, el problema no es el código, sino las capas. SwiftUI tiende a “aplanar” la jerarquía de vistas para optimizar el rendimiento, pero a veces crea contenedores intermedios que no esperas.

Cuando tu aplicación esté corriendo (en simulador o dispositivo real), pulsa el botón Debug View Hierarchy en la barra de control de debug de Xcode (el icono que parecen tres rectángulos apilados).

Qué buscar aquí:

  1. Vistas ocultas: A veces una vista tiene frame(width: 0) o está tapada por otra vista con fondo transparente. Gira el modelo 3D para ver qué hay detrás.
  2. Z-Index Incorrecto: En watchOS y iOS, el orden de apilamiento en un ZStack es crucial. Si un botón no responde, verifica aquí si hay una vista transparente encima interceptando los toques.
  3. Tamaños de Frame: Selecciona una vista en el inspector y ve al panel de propiedades (derecha). Verifica si el sistema le está dando el tamaño que crees que tiene. Un error común es un Text que se trunca porque su contenedor padre tiene un ancho fijo.

4. El Secreto Mejor Guardado: Self._printChanges()

Si solo te llevas una cosa de este artículo, que sea esta.

Uno de los problemas más difíciles en SwiftUI es el re-renderizado excesivo. ¿Por qué se actualiza toda mi lista cuando solo cambié un ícono? Antes de iOS 15, teníamos que adivinar. Ahora, tenemos un arma secreta.

Dentro de tu propiedad body, puedes invocar un método estático (y subrayado, lo que indica que es interno/beta pero seguro para debug) que imprime en consola qué causó la actualización de la vista.

struct UserProfileView: View {
    @ObservedObject var viewModel: ProfileViewModel

    var body: some View {
        let _ = Self._printChanges() // <--- EL ARMA SECRETA
        
        VStack {
            Text(viewModel.name)
            // ... resto de la vista
        }
    }
}

Interpretando la salida en consola:

  • UserProfileView: @self, changed. -> La vista se recreó porque su inicializador fue llamado con nuevos parámetros (un struct padre cambió).
  • UserProfileView: _viewModel changed. -> El @Published dentro de tu ViewModel disparó el cambio.

Nota Pro: Recuerda eliminar esta línea antes de enviar a producción, ya que puede afectar ligeramente el rendimiento y ensuciar los logs.


5. Debugging “In-Line” y Breakpoints en SwiftUI

Poner breakpoints en SwiftUI es confuso porque el body es una propiedad calculada que devuelve un tipo opaco (some View). No hay un flujo lineal claro donde “parar”.

La técnica del let _ = ...

Como vimos con _printChanges, podemos ejecutar código arbitrario dentro del body asignándolo a una variable anónima.

var body: some View {
    let _ = print("Renderizando Body con valor: \(valor)")
    Text("Hola")
}

Breakpoints de Acción (Sin Pausa)

Parar la ejecución (pausar la app) en aplicaciones reactivas a veces altera el comportamiento, especialmente si estás debuggeando animaciones o concurrencia.

  1. Haz clic en el número de línea (gutter) para crear un breakpoint.
  2. Haz clic derecho > Edit Breakpoint.
  3. Añade una Action de tipo “Log Message”.
  4. Escribe: Valor de x: @x@.
  5. Marca la casilla “Automatically continue after evaluating actions”.

Ahora tienes logs en tiempo real sin ensuciar tu código con print().

LLDB: po vs p vs v

Cuando el breakpoint sí pausa la ejecución, usas la consola LLDB.

  • po variable: (Print Object) Es el estándar, pero es lento porque evalúa la expresión completa.
  • p variable: Similar a po pero muestra la representación cruda.
  • v variable: (View frame variable) Usa este. Lee la memoria directamente sin compilar ni evaluar código. Es instantáneo y evita timeouts comunes al inspeccionar ViewModels grandes en SwiftUI.

6. Coloreando los Marcos (The Rainbow Debugging)

Cuando el layout no tiene sentido (espacios en blanco extraños, alineaciones fallidas), la mejor herramienta es visual.

Crea una extensión de View simple pero potente para visualizar los frames:

extension View {
    func debugBorder(_ color: Color = .red) -> some View {
        self.border(color, width: 1)
    }
    
    func debugBackground() -> some View {
        self.background(Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1),
            opacity: 0.3
        ))
    }
}

Úsalo en tu código:

VStack {
    HeaderView().debugBorder(.blue)
    Spacer().debugBackground() // ¿Por qué este spacer es tan grande?
    FooterView().debugBorder(.green)
}

Esto revela inmediatamente si un Spacer está empujando contenido fuera de la pantalla o si un Text tiene un frame más grande que su contenido.


7. Retos Específicos: macOS y watchOS

El núcleo de SwiftUI es compartido, pero los comportamientos de la plataforma no.

macOS: El infierno del redimensionado

En macOS, las ventanas son redimensionables por el usuario. Un bug común es asumir un tamaño fijo.

  • Debug Tip: En Xcode Previews para macOS, usa el modificador .previewLayout(.fixed(width: 500, height: 300)) para probar tamaños forzados.
  • Usa el Environment Override mientras corres la app en Mac para cambiar el tamaño de texto dinámico y el modo oscuro al vuelo.

watchOS: La lentitud del simulador

Debuggear en el simulador de watchOS puede ser dolorosamente lento, haciendo que parezca que tu código de red tiene bugs cuando solo es latencia.

  • Debug Tip: Conecta tu Apple Watch físico. En Xcode, ve a Devices and Simulators y marca “Connect via Network”. El debugging inalámbrico en el reloj ha mejorado mucho en Xcode 15+.
  • Si usas WKApplicationRefreshBackgroundTask (actualizaciones en segundo plano), usa la opción de Xcode Debug > Simulate Background Fetch para forzar el evento sin esperar horas.

8. Instruments: Cuando la UI se Congela (Hangs)

Si tu aplicación hace scroll y se siente “pesada” o el reloj muestra la rueda de carga, tienes un problema de rendimiento (Hangs). LLDB no te ayudará aquí; necesitas Instruments.

  1. En Xcode, presiona Cmd + I (Profile).
  2. Selecciona SwiftUI (si está disponible en tu versión) o Time Profiler.

El culpable habitual: Computación en el Body

El body de una vista se invoca frecuentemente. Si haces esto, matarás el rendimiento:

// ❌ MAL: Cálculo pesado dentro del body
var body: some View {
    let datosProcesados = filtrarYOrdenar(datos) // Esto corre en el Main Thread
    List(datosProcesados) { ... }
}

Instruments te mostrará grandes barras azules en el hilo principal (Main Thread). La solución es mover ese cálculo al ViewModel y publicar el resultado final, o usar .task para calcularlo asíncronamente.


9. Debugging de Flujo de Datos: @State vs @StateObject

El error más común que veo en Code Reviews y que causa bugs difíciles de rastrear es la confusión entre StateObject y ObservedObject.

  • El Bug: La vista se recarga y pierde los datos que el usuario escribió.
  • La Causa: Usaste @ObservedObject para inicializar un ViewModel dentro de una vista que no es la propietaria.
struct VistaPadre: View {
    var body: some View {
        // Cada vez que VistaPadre se redibuja, VistaHija crea un NUEVO ViewModel
        VistaHija() 
    }
}

struct VistaHija: View {
    // ❌ ERROR: Esto reinicia el VM en cada render del padre
    @ObservedObject var vm = MiViewModel() 
    
    // ✅ CORRECCIÓN: Usa @StateObject para mantener la vida del objeto
    // @StateObject var vm = MiViewModel()
}

Para debuggear esto, pon un print("init viewModel") en el init de tu clase ViewModel. Si ves ese log múltiples veces mientras interactúas con la UI, tienes una fuga de propiedad de objetos.


Conclusión: La Paciencia es tu Mejor Herramienta

Debuggear en SwiftUI requiere dejar de pensar linealmente y empezar a pensar estructuralmente. Las herramientas que Xcode nos ofrece hoy son años luz mejores que las que teníamos en las primeras versiones de SwiftUI.

Recuerda el flujo de trabajo del experto:

  1. Usa Previews para iteración rápida.
  2. Usa View Hierarchy para problemas de layout.
  3. Usa Self._printChanges() para entender por qué se actualiza la vista.
  4. Usa Instruments solo cuando notes problemas de rendimiento.

SwiftUI es potente, y con estas herramientas, tú tienes el control total sobre lo que ocurre en la pantalla, ya sea en la muñeca, en el bolsillo o en el escritorio.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

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

Previous Article

Cómo detectar la rotación del dispositivo en SwiftUI

Next Article

Cómo mostrar un popover en SwiftUI

Related Posts