PreferenceKey en SwiftUI
PreferenceKey en SwiftUI

PreferenceKey en SwiftUI en Español

PreferenceKey en SwiftUI lo usamos para poder enviar información a través de la jerarquía de vistas. En lugar de enviar esta información hacía las subvistas, la enviamos hacía arriba. Hacía la vista padre o anteriores. Por ejemplo, cuando queremos modificar el título del NavigationView.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende SwiftUI desde cero y aprende a usar el protocolo PreferenceKey
Aprende SwiftUI desde cero y aprende a usar el protocolo PreferenceKey

PreferenceKey en SwiftUI nos sirve para pasar información a través de la jerarquía de vistas. Ya lo vimos con el Property Wrapper EnvironmentObject, es decir, vimos cómo en una jerarquía de vistas de A-> B-> C, podríamos usar un ViewModel que definíamos en la vista A y que acabábamos usando en la subvista C (sin que B supiera de su existencia). Mágicamente lo metíamos en el environment en la vista A y luego lo podíamos usar en la vista C. Sino has visto el video te aconsejo que le eches un vistazo.

Pues bien, muchas veces queremos hacer lo mismo, pero en lugar de enviar información y que esta viaje a las subvistas, queremos que viajen en el otro sentido, queremos que viajen hacía arriba, hacía la vista padre o anteriores.

Un ejemplo muy claro es cuando usamos NavigationView, al querer modificar el title del navigation view, no lo hacemos usando el modificador navigationTitle directamente en NavigationView, sino que lo hacemos en una de sus subvistas
y este cambio se propaga hacía arriba a través de la jerarquía de vistas. Vamos a ver un ejemplo:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Suscríbete a SwiftBeta")
                    .padding()
            }
            .navigationTitle("SwiftBeta")
        }
    }
}

Para poder usar los PreferenceKey, no es tan fácil como usar el property wrapper @EnvironmentObject. Aquí tenemos que crear una struct y conformar el protocolo PreferenceKey que tiene 2 requerimientos, que son:

- Una variable llamada defaultValue, donde añadiremos un valor por defecto.
- Una función llamada reduce, esta función será llamada cuando dos (o más) vistas del mismo nivel quiran modificar un mismo valor de una de sus vistas padre. Aquí podremos aplicar una lógica para combinar esta información, quedarse con el primero, con el último, etc.
Para que se entienda mejor he creado el siguiente ejemplo:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Suscríbete a SwiftBeta 🚀")
                    .padding()
                    .navigationTitle("SwiftBeta 1")
                Text("Aprende SwiftUI 📚")
                    .padding()
                    .navigationTitle("SwiftBeta 2")
            }
        }
    }
}

Al poner dos modificadores para cambiar el título de la navegación ¿Qué título crees que se acabará mostrando? En este caso se mostraría "Título 1". Se queda con el primer valor usado con el modificador navigationTitle.

Lo que vamos hacer es simular este comportamiento, y para ello vamos a crear una vista que simule un NavigationView desde cero y vamos a crear nuestro propio modificador navigationTitle, a la vista la vamos a llamar CustomNavigationView y al modificador customNavigationTitle.

Para empezar, vamos a crear la vista:

struct CustomNavigationView<Content: View>: View {
    
    @State private var title: String = "Navigation View"
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.largeTitle)
                .bold()
            GeometryReader { _ in
                ScrollView {
                    content
                }
            }
        }
        .padding(.horizontal)
    }
}

Ahora vamos a usar nuestro CustomNavigationView en ContentView, tan solo debemos sustituir el NavigationView por CustomNavigationView:

struct ContentView: View {
    
    var body: some View {
        CustomNavigationView {
            VStack {
                Text("Suscríbete a SwiftBeta 🚀")
                    .padding()
                    .navigationTitle("SwiftBeta 1")
                Text("Aprende SwiftUI 📚")
                    .padding()
                    .navigationTitle("SwiftBeta 2")
            }
        }
    }
}
No es igual que un NavigationView nativo, pero el propósito del post de hoy es ver como funciona el modificador navigationTitle

Lo que haremos a continuación será crear una struct que conforme el protocolo PreferenceKey, vamos a llamar a esta struct TitleKey:

struct CustomTitleKey: PreferenceKey {
    static var defaultValue: String = ""
    
    static func reduce(value: inout String, nextValue: () -> String) {
        value = nextValue()
    }
}

Le hemos dado un valor por defecto a la variable defaultValue, y en el método reduce, lo que hacemos es que si varias vistas quieren modificar el title de nuestro CustomNavigationView, nos quedamos con el último modificador (recuerda, con el modiviador navigationTitle el primero que modificaba el título es el que predominaba y los demás no hacían nada).

Ahora lo que tenemos que hacer es usar nuestra struct TitleKey, y ver cómo hacemos para que cuando una subvista modifique el título de nuestro CustomNavigationView se actualice correctamente. Lo primero que vamos hacer es usar el modificador .onPreferenceChange, este modificador nos sirve para escuchar cuando hay un cambio en el valor de nuestra PreferenceKey y añadir una acción, en nuestro caso queremos observar TitleKey que es la struct que acabamos de crear.

Para escuchar estos cambios, vamos a nuestro CustomNavigationView y añadimos el modificador onPreferenceChange

struct CustomNavigationView<Content: View>: View {
    
    @State private var title: String = "Navigation View"
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.largeTitle)
                .bold()
            GeometryReader { _ in
                ScrollView {
                    content
                }
            }
        }
        .padding(.horizontal)
        .onPreferenceChange(CustomTitleKey.self) { (value: CustomTitleKey.Value) in
            print("Value \(value)")
            title = value
        }
    }
}

Si te fijas, hemos puesto el modificador con el tipo CustomTitleKey, ya que es el tipo que queremos escuchar cuando alguien realice un cambio en su valor.
Cada vez que alguien modifica este valor, lo asignamos a la propiedad title.
De esta manera se actualiza el texto de nuestro CustomNavigationView cada vez que alguien lo modifica. ¿Pero quién lo modifica? Vamos a ir a nuestro ContentView y donde antes usábamos navigationTitle ahora usaremos

.preference(key: CustomTitleKey.self, value: "SwiftBeta 1") ContentView

y quedaría de la siguiente manera:

struct ContentView: View {
    
    var body: some View {
        CustomNavigationView {
            VStack {
                Text("Suscríbete a SwiftBeta 🚀")
                    .padding()
                    .preference(key: CustomTitleKey.self, value: "SwiftBeta 1")
                Text("Aprende SwiftUI 📚")
                    .padding()
                    .preference(key: CustomTitleKey.self, value: "SwiftBeta 2")
            }
        }
    }
}

Y verás en el live preview del canvas como aparece el título de nuestro CustomNavigationView con "SwiftBeta 2".
Dentro del método reduce de nuestro PreferenceKey podemos cambiar la lógica y mostrar por ejemplo el primer valor (en lugar del último como estamos mostrando ahora mismo). Vamos a ver el cambio:

struct CustomTitleKey: PreferenceKey {    
    static var defaultValue: String = ""

    static func reduce(value: inout String, nextValue: () -> String) {
        if !value.isEmpty {
            return
        }
        value = nextValue()
    }
}

o por ejemplo, si quisieramos concatenar todos los valores, podríamos hacerlo de la siguiente manera:

Y el título en nuestra CustomNavigationView sería "SwiftBeta 1 SwiftBeta 2".

struct CustomTitleKey: PreferenceKey {
    typealias Value = String
    
    static var defaultValue: String = ""

    static func reduce(value: inout String, nextValue: () -> String) {
        value = value + " " + nextValue()
    }
}

Creamos nuestro propio modificador en SwiftUI

Si queremos evitar usar el modificador .preference y recordar siempre el nombre de la struct, podemos añadir una mejora a nuestro código. Para ello, creamos una struct que conforme el protocolo ViewModifier:

struct CustomNavigationTitle: ViewModifier {
    
    private var title: String
    
    init(title: String) {
        self.title = title
    }
    
    func body(content: Content) -> some View {
        content
            .preference(key: CustomTitleKey.self, value: title)
    }
}

y creamos una extension en View:

extension View {
    func customNavigationTitle(title: String) -> some View {
        modifier(CustomNavigationTitle(title: title))
    }
}

de esta manera, ahora podríamos usar nuestro nuevo modificador .customNavigationTitle y evitar errores o bugs en nuestro código:

struct ContentView: View {

    var body: some View {
        CustomNavigationView {
            VStack(alignment: .leading) {
                Text("Suscríbete a SwiftBeta 🚀")
//                    .preference(key: CustomTitleKey.self, value: "SwiftBeta 1")
                    .customNavigationTitle(title: "SwiftBeta 1")
                    .padding()
                Text("Aprende SwiftUI 📚")
//                    .preference(key: CustomTitleKey.self, value: "SwiftBeta 2")
                    .customNavigationTitle(title: "SwiftBeta 2")
                    .padding()
            }
        }
    }
}

Tan solo deberíamos usar .customNavitationTitle con el title que queremos. Con esto nos ahorramos código, y posibles bugs.