Otro gran problema con las macros de @Observable es como gestionar @AppStorage. A continuación veremos como podemos adaptar nuestro código para usar @AppStorage en @Observable y así poder tener una variable de entorno que pueda guardarse de forma automática.
¿Qué es @AppStorage?
@AppStorage es un property wrapper de SwiftUI que permite vincular una variable directamente con UserDefaults, la manera más sencilla de guardar pequeños datos de forma serializados (no estructurados) de tipo Bool, Int, Float, Double, String y también objetos como Date y Data. Se usa comúnmente para guardar datos de preferencias de usuario (de ahí su nombre UserDefaults).
@AppStorage elimina prácticamente toda la complicación a la hora de guardar esta información del usuario. Simplemente hay que declararlo en una vista de SwiftUI con un valor por defecto y ya estaría. Ahora, cada vez que se cambia el valor ya se guarda de forma automática, siempre que abramos la App, el valor será el que guardamos.
struct ContentView: View {
@AppStorage("nombreUsuario") var nombreUsuario: String = "Lucas"
var body: some View {
VStack {
Text("Nombre de usuario: \(nombreUsuario)")
Button("Cambiar Usuario") {
nombreUsuario = "Grijander"
}
}
}
}
@AppStorage global
Estaría muy bien que estas variables pudieran ser accesibles de manera global. Si por ejemplo usamos una clase para gestionar datos globales de nuestra app, sería muy cómodo que aparecieran ahí.
@Observable
class UserSettings {
@AppStorage("username") var username: String = "Lucas" // ❌ ERROR
}
Un truco podría ser, des de la vista configurar un .onChange para observar si nombreUsuario se está cambiando. Cuando se cambie podríamos actualizar el valor de AppData para ser utilizado a posteriori. Sin embargo no me parece una opción muy mantenible ya que estaríamos poniendo la responsabilidad de actualizar este valor a una vista (o varias, cosa que sería aún más problemático).
Una posible solución podría ser la siguiente. Primero de todo vamos a encapsular nuestros @AppStorage en una clase que podemos llamar Storage.
final class Storage {
@AppStorage("nombreUsuario") var nombreUsuario: String = "Lucas"
}
Luego en nuestra AppData la inicializamos de forma privada
@Observable
class AppData {
private let storage = Storage()
}
Para usar los valores de la clase Storage las deberemos exponer, pero lo haremos con didSet. Las variables que tienen didSet y willSet para observar cambios son almacenadas y hay que inicializarlas. La cosa quedaría así:
@Observable
class AppData {
private let storage = Storage()
var nombreUsuario: String {
didSet {
// Observamos el cambio y actualizamos storage
storage.nombreUsuario = nombreUsuario
}
}
init() {
// Inicializamos la variable con el valor guardado en storage
self.nombreUsuario = storage.nombreUsuario
}
}
Veamos un ejemplo de uso:
import SwiftUI
struct ContentView: View {
@Environment(AppData.self) var appData
var body: some View {
VStack {
Text("Nombre de usuario: \(appData.nombreUsuario)")
Button("Cambiar a Grijander") {
appData.nombreUsuario = "Grijander"
}
Button("Cambiar a Lucas") {
appData.nombreUsuario = "Lucas"
}
}
}
}
#Preview {
ContentView()
.environment(AppData())
}
Ventajas de esta estrategia:
- Encapsulamos fuera de las vistas la responsabilidad de gestionar el guardado de estas variables.
- Más fácil de gestionar la creación de nuevas variables en un único lugar.
- Al ser variables almacenadas de @Observable, cualquier cambio es propagado en la vista que esté usándola.
- Accesible des de cualquier punto de la app.
Uso de enums
Típicamente querremos guardar preferencias de usuario que son valores concretos de una enumeración, por ejemplo los posibles esquemas de color de una app:
enum ColorModeOption: String, CaseIterable {
case light = "Light"
case dark = "Dark"
case system = "System"
}
Cómo lo hacemos para guardar estos valores usando @AppStorage sin tener que crear una variable extra para almacenar el texto y recuperar el objeto LanguageOption en cuestión?
creamos la variable como antes en nuestra clase Storage que ahora tendrá este aspecto. Nótese que estamos guardando el rawValue String de la enumeración.
final class Storage {
@AppStorage("nombreUsuario") var nombreUsuario: String = ""
@AppStorage("colorMode") var colorMode: String = ColorModeOption.system.rawValue
}
Luego creamos la variable almacenada en AppData
var colorMode: ColorModeOption {
didSet {
storage.colorMode = colorMode.rawValue
}
}
finalmente la inicializamos usando el inicializador de las enumeraciones a partir del rawValue
init() {
self.nombreUsuario = storage.nombreUsuario
self.colorMode = .init(rawValue: storage.colorMode) ?? .system
}