Saltar al contenido

Depurar vistas de SwiftUI

Depurar vistas de SwiftUI

Alexander, desarrollador sénior de iOS de tecnologiapc, habla sobre las complejidades de trabajar con View en SwiftUI.

SwiftUI hace todo lo posible para evitar que la interfaz de usuario se retrase al volver a dibujar. Si queremos comprender mejor cómo funciona el marco bajo el capó, vale la pena investigar un poco más.

Básicamente, nos interesan dos eventos: cuándo se recrea la Vista y cuándo se le pide que vuelva a dibujar el cuerpo. Para trabajar con estos eventos, creé un contenedor que le permite rastrearlos:

import SwiftUI

public struct DebugView: View {
    private let view: MainView
    private let logType: LogType

    private enum LogType {
        case onlyDescription(String)
        case descriptionAndDumpView(String)
        case dumpView
    }

    private var about: String {
        switch logType {
            case let .onlyDescription(description):
                return "(description)"
            case let .descriptionAndDumpView(description):
                return "(description): (view)"
            case .dumpView:
                return "(view)"
            }
        }

    public init(view: MainView, description: String?, dumpView: Bool = true) {
        self.view = view
        if let description = description {
            if dumpView {
                logType = .descriptionAndDumpView(description)
            } else {
                logType = .onlyDescription(description)
            }
        } else {
            logType = .dumpView
        }
        print("init: (about)")
    }

    public var body: some View {
        print("body: (about)")
        return view
    }
}

extension View {
    public func debug() -> DebugView {
        return DebugView(view: self, description: nil)
    }

    public func debug(_ description: String, dumpView: Bool = false) -> DebugView {
        return DebugView(
            view: self,
            description: description,
            dumpView: dumpView
        )
    }
}

Aquí está su esencia.

En general, este es un proxy de visualización. Intentemos trabajar con esto. Creemos un patio de juegos en el que veremos el trabajo con SwiftUI Views en dinámica:

import SwiftUI
import PlaygroundSupport

private struct MyListView: View {
    @State var numberOfViews: Int = 1
    var body: some View {
        VStack(spacing: 30) {
            ForEach(0..

Cuando comience en los registros veremos

init: MyListView
body: MyListView
init: Text: 0
body: Text: 0

Parece razonable, cuando se inicia la aplicación, el sistema primero crea un MyListView, toma el cuerpo de él, ve que se necesita un texto, lo crea y luego solicita un cuerpo.

Haga clic una vez en el botón numberOfViews. Se agregará lo siguiente a los registros:

init: Text: 0
init: Text: 1
body: Text: 1

Pero esto ya es interesante: vemos que MyListView no se recrea, lo que parece lógico, pero el texto se crea 2 veces (la vista pasa a 2 después de aumentar el contador), pero el cuerpo se solicitó solo para un nuevo elemento en la pantalla.

Si volvemos a hacer clic en el botón numberOfViews, veremos el esperado

init: Text: 0
init: Text: 1
init: Text: 2
body: Text: 2

Aquellos. solo se llamará al cuerpo para nuevos elementos.

Tratemos de entender cómo y por qué el sistema funciona de esta manera.

Creemos nuestra vista - MyTextView y asegurémonos de que el contenido único se genere con cada creación:

private struct MyText: View {
    var body: some View {
        return Text("(Int.random(in: 0...100))")
    }

Cambiar la llamada a

ForEach(0..

Al principio vemos en los registros

init: MyListView
body: MyListView
init: MyText: 0
body: MyText: 0

Después de hacer clic en el botón numberOfViews

init: MyText: 0
init: MyText: 1
body: MyText: 1

Al mismo tiempo, esperamos que el número del elemento que se muestra arriba cambie en la pantalla (después de todo, aleatorio), pero en realidad el número no cambia. Aquellos. el sistema crea MyText, pero aparentemente lo descarta sin volver a dibujar. ¿Porque? Porque cree que todos MyText son iguales, ya que entonces esta estructura no implementa Equatable.

Comprobemos nuestra hipótesis:

private struct MyTextEquatable: View, Equatable {
    private let id: Int
    init() {
        id = Int.random(in: 0...100)
    }
    var body: some View {
        return Text("(id)")
    }
}

Cambiar la llamada a

ForEach(0..

Primer comienzo:

init: MyListView
body: MyListView
init: MyTextEquatable: 0
body: MyTextEquatable: 0

Haga clic en el botón:

init: MyTextEquatable: 0
body: MyTextEquatable: 0
init: MyTextEquatable: 1
body: MyTextEquatable: 1

Funcionó, se puede ver no solo en los registros, sino también en la vista previa: después de cada clic en el botón, todos los números cambian. Ahora, todo parece ir bien, cada vez que se actualiza Vista, el sistema crea sus hijos (es por eso que los inicializadores para Vista deben ser lo más livianos posible, no debe poner ninguna lógica "pesada" en el constructor) y compare usando el protocolo Equatable si el estado interno ha cambiado; de lo contrario, el sistema considera que no es necesario volver a dibujar, porque la vista no se ha actualizado.

Comprobemos, creemos lo siguiente:

private struct StupidView: View, Equatable {
    private let id: Int
    init(id: Int) {
        self.id = id
    }

    var body: some View {
        return Text("(id)")
    }

    static func == (lhs: StupidView, rhs: StupidView) -> Bool {
        return false
    }
}

Cambiar la llamada a

ForEach(0..

Al principio

init: MyListView
body: MyListView
init: StupidView: 0
body: StupidView: 0

Después de hacer clic

init: StupidView: 0
init: StupidView: 1
body: StupidView: 1

¡Qué diablos, siempre devolvemos falso al comparar! Para depurar la vista SwiftUI, deberá crear un proyecto completo y crear un nuevo archivo con el contenido allí:

import SwiftUI

private struct MyListView: View {
    @State var numberOfViews: Int = 1
    var body: some View {
        VStack(spacing: 30) {
            ForEach(0.. Bool {
        return false
    }
}

struct EquatableSwiftUIStupid_Previews: PreviewProvider {
    static var previews: some View {
        MyListView()
    }
}

Si llama al menú contextual del botón de reproducción en la vista previa, podrá hacer una vista previa de depuración.

SwiftUI-Views tecnologiapc

¿Qué vemos al depurar? Los puntos de interrupción se disparan dentro del cuerpo de var, pero no dentro de static func ==: por alguna razón, el sistema no llama a nuestro método para comparar. SwiftUI muy inteligente. Finalmente, encontré una forma de evitar esto: use el contenedor Holder en lugar de Int. Aparentemente, en este caso, SwiftUI todavía decide confiar en la implementación proporcionada, ya que ya no tenemos un tipo de valor simple.

Vamos a revisar:

private class Holder {
    var id: Int

    init(id: Int) {
        self.id = id
    }
}

private struct StupidViewWithHolder: View, Equatable {
    private let holder: Holder
    init(holder: Holder) {
        self.holder = holder
    }

    var body: some View {
        return Text("(holder.id)")
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        return false
    }
}

Cambiar la llamada a

ForEach(0..

Al principio

init: MyListView
body: MyListView
init: StupidViewWithHolder: 0
body: StupidViewWithHolder: 0

Después de hacer clic

init: StupidViewWithHolder: 0
body: StupidViewWithHolder: 0
init: StupidViewWithHolder: 1
body: StupidViewWithHolder: 1

¡Finalmente obtuvimos un rediseño real de los elementos!

Tenga en cuenta que esto no debe tenerse en cuenta durante el desarrollo, ya que se trata de un comportamiento interno de SwiftUI que puede cambiar en cualquier momento. Pero el conocimiento puede ayudarlo a comprender lo que, en principio, puede salir mal y no perder mucho tiempo depurando vistas realmente complejas. Ahora sabemos de otra trampa SwiftUI, además de que la nueva herramienta de depuración View ha demostrado su valía. Espero que haya sido útil 🙂

Puede descargar Playground con todos los ejemplos del artículo aquí: Playground.

Si encuentra un error, seleccione un fragmento de texto y presione Ctrl + Entrar...