Search code examples
swiftswiftuiobservable

Does anyone know why Picker from my view file won't accept the binding version of an observable object property using @Observable


I am not entire sure why using the @Observable tag does not work the same as using the 'old' way which is using @Published and ObservableObject and @StateObject. I am just curious why that wouldn't work since using @Observable is supposed to be the new way.

Also I am new to SwiftUI.
I am trying to get the Picker from the view screen to update the variable currentSelection from SettingsViewModel on any user interactions with the picker.

import Foundation

@Observable
class SettingsViewModel {
    
    var currentSelection : ColorOptionEnum
    var dataHandler : DataHandler
    
    init() {
        let localdataHandler: DataHandler = .init()
        currentSelection = localdataHandler.currentBibleVersion
        dataHandler = localdataHandler
    }
}

-------------

import SwiftUI

struct SettingsView: View {
    var settingsViewModel : SettingsViewModel = .init()

    
    var body: some View {

        Form {
            Text("Mode")
            Picker(
                "Mode",
                selection: $settingsViewModel.currentSelection // GETTING Error here ( cannot find '$settingsViewModel' in scope)
            ) {
                Text(
                    "Green"
                )
                .tag(
                    ColorOptionEnum.Green
                )
                Text(
                    "Blue"
                )
                .tag(
                    ColorOptionEnum.Blue                )

                            }
            
        }
    }
}

#Preview {
    SettingsView()
}

This version works

import Foundation

class SettingsViewModel: ObservableObject {
    
    @Published var currentSelection : ColorOptionEnum
    var dataHandler : DataHandler
    
    init() {
        let localdataHandler: DataHandler = .init()
        currentSelection = localdataHandler.currentBibleVersion
        dataHandler = localdataHandler
    }
}

-------------

import SwiftUI

struct SettingsView: View {
    @StateObject var settingsViewModel : SettingsViewModel = .init()

    
    var body: some View {

        Form {
            Text("Mode")
            Picker(
                "Mode",
                selection: $settingsViewModel.currentSelection // No error
            ) {
                Text(
                    "Green"
                )
                .tag(
                    ColorOptionEnum.Green
                )
                Text(
                    "Blue"
                )
                .tag(
                    ColorOptionEnum.Blue             
                )

            }
            
        }
    }
}

#Preview {
    SettingsView()
}

Solution

  • The error cannot find '$settingsViewModel' in scope is because Picker requires a binding for its selection parameter. When you use a var settingsViewModel : SettingsViewModel = .init(), you don't get a binding to your model.

    While @State does get you a binding, it should only be used if it's considered to be the owner of the object. Otherwise, if the owner is a parent view, you can accept it as a Binding in your child view or when using @Observable, you can accept it as an object and get a binding to it in your child view, using @Bindable.

    Note: Since your code wasn't reproducible due to lacking the DataHandler class and the ColorOptionEnum, I commented it out in the example code below and added a sample enum to make it work:

    import Foundation
    import SwiftUI
    
    enum ColorOptionEnum {
        case green, blue, orange // <- cases should be lowercased
        
        var color: Color {
            switch self {
                case .green: return .green
                case .blue: return .blue
                case .orange: return .orange
            }
        }
    }
    
    @Observable
    class DemoSettings {
        
        var currentSelection : ColorOptionEnum = .green
        // var dataHandler : DataHandler
        
        // init() {
        // let localdataHandler: DataHandler = .init()
        // currentSelection = localdataHandler.currentBibleVersion
        // dataHandler = localdataHandler
        // }
    }
    
    //Parent view
    struct DemoSettingsRootView: View {
        
        //State value
        @State private var demoSettings = DemoSettings() // <- Parent view owns the settings object
        
        //Body
        var body: some View {
            
            DemoSettingsView(settings: demoSettings) // <- Settings passed to child view as an (observable) object
            
        }
    }
    
    //Child view
    struct DemoSettingsView: View {
        
        //Parameters
        var settings : DemoSettings // <- settings accepted from parent as an object
        
        //Body
        var body: some View {
            
            @Bindable var settings = settings // <- Here, get a binding to the observable settings object
            
            Text("Selected mode: \(String(describing: settings.currentSelection).capitalized)")
                .foregroundStyle(settings.currentSelection.color)
            
            Form {
                Picker("Mode", selection: $settings.currentSelection) { // <- use the binding for the selection parameter
                    Text("Green")
                        .tag(ColorOptionEnum.green)
                    Text("Blue")
                        .tag(ColorOptionEnum.blue)
                }
            }
        }
    }
    
    #Preview {
        DemoSettingsRootView()
    }
    

    enter image description here