Hasta ahora cuando teníamos una clase marcada como @ObservableObject, podíamos usar sus variables @Published como Binding. Por ejemplo en el caso de tener un PickerView.
Veamos el siguiente caso: Tengo una clase AppData que usaré como environmentObject. En ella tengo una variable selectedOption para que sea usada por un PickerView.
enum ColorSchemeOption: String, CaseIterable {
case light
case dark
case system
}
class AppData: ObservableObject {
@Published var selectedOption: ColorSchemeOption = .system
...
}
Para tener este objeto en el environment, se lo tenemos que pasar como .environmentObject a la vista más externa. Esto solo hay que hacerlo una vez, ya que las vistas hijas (las que estén dentro de environment) ya pueden acceder a ese objeto de entorno automáticamente (si se lo decimos).
@main
struct MyApp: App {
@StateObject private var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData)
}
}
}
Para usar AppData dentro de nuestra vista podemos hacerlo como se muestra a continuación. (Nótese que llamamos al objeto appData por simplicidad pero podríamos llamarlo como quisiéramos).
Aquí nuestro PickerView usa y cambia appData.selectedOption usando $ para acceder al valor proyectado.
struct ContentView: View {
@EnvironmentObject var appData: AppData
var body: some View {
VStack {
Picker("Color Scheme", selection: $appData.selectedOption) {
ForEach(ColorSchemeOption.allCases, id: \.self) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
Pasar a @Observable
Pasar a la nueva estructura de datos de Swift @Observable es tan fácil como añadir esta palabra clave al objeto de entorno que queramos, quitar el protocolo ObservableObject y quitar también todas las menciones @Published, ya que automáticamente todas las variables de una clase @Observable ya son de tipo @Published.
Nuestra clase quedaría así:
@Observable
class AppData {
var selectedOption: ColorSchemeOption = .system
...
}
Para pasar este un objeto @Observable al entorno hay que hacerlo de forma ligeramente diferente, ya no es un .environmentObject, es sencillamente .environment.
@main
struct MyApp: App {
@State private var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appData)
}
}
}
Para usar una variable de tipo environment en la vista también tenemos que cambiar un poco la sintaxis…
struct ContentView: View {
@Environment(AppData.self) private var appData
var body: some View {
VStack {
Picker("Color Scheme", selection: $appData.selectedOption) {
ForEach(ColorSchemeOption.allCases, id: \.self) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
Error con Bindings y posibles soluciones
Esto en principio no debería fallar, sin embargo, tendremos un error, ya que no podremos acceder al valor proyectado debido a como és (por ahora) el código extra que genera la macro @Observable. Si buscamos información o preguntamos nuestra amiga la IA, nos ofrecerá varias soluciones.
Una muy lenta y que nos obliga a escribir más código de la cuenta, pasa por crear una variable auxiliar calculada de tipo Binding que vaya actualizando la variable original. Esta variable calculada sí la podríamos usar en vistas o elementos que necesiten Binding.
struct ContentView: View {
@Environment(AppData.self) private var appData
var selectedOptionBinding: Binding<ColorSchemeOption> {
Binding(
get: { appData.selectedOption },
set: { appData.selectedOption = $0 }
)
}
...
}
Esto lo podríamos poner también como variable computada dentro de nuestro AppData (modificanto ligeramente el código), pero igualmente es una mala solución: es un proceso lento y nos obliga a añadir código para cada variable que queramos usar como Binding.
@Bindable
La solución más elegante es redeclarar appData como @Bindable en la vista que lo necesite. De esta manera ya podremos usar libremente cualquier variable de un objecto @Observable como Binding sin necesidad de hacer nada más.
Os muestro el ejemplo:
struct ContentView: View {
@Environment(AppData.self) private var appData
var body: some View {
@Bindable var appData = appData
VStack {
Picker("Color Scheme", selection: $appData.selectedOption) {
ForEach(ColorSchemeOption.allCases, id: \.self) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
Et voilà. Una solución rápida y elegante para nuestros problemas con Bingings y @Observable!