Search code examples
iosswiftswiftui

SwiftUI - Cannot Convert ObservedObject<UserData>.Wrapper to Binding<UserData> in RegistrationView


I am developing a SwiftUI-based registration flow in my app, where a user progresses through several steps to complete their registration. The issue I've encountered involves passing an instance of UserData, which is an ObservableObject, to various child views as a Binding. Specifically, I'm stuck with the error message: "Cannot convert value of type 'ObservedObject.Wrapper' to expected argument type 'Binding'" in my RegistrationView.

I have a RegistrationView that controls the registration process and several child views for each step of the registration (like EmailRegistrationStepView, PhoneNumberStepView, etc.). These child views need to update the shared UserData object.

Despite following the standard SwiftUI approach of using @ObservedObject and passing the data as a Binding using the $ syntax, I keep getting a type conversion error. This error occurs when I try to pass userData from the RegistrationView to its child views.

Attempted Solutions & Difficulties:

I've double-checked that userData is correctly declared as an @ObservedObject and that I'm using the $ prefix for creating a Binding. I've cleaned the build folder and restarted Xcode. I'm using Xcode Version 15.0.1. I attempted to simplify the code to isolate the issue, but the error persists.

Code:

import SwiftUI

struct RegistrationView: View {
    enum RegistrationStep {
        case emailAndPassword
        case phoneNumber
        case verificationCode
        ...
    }

    @State private var registrationStep: RegistrationStep = .emailAndPassword
    @ObservedObject var userData: UserData

    var body: some View {
        VStack {
            switch registrationStep {
            case .emailAndPassword:
                EmailRegistrationStepView(nextStep: $registrationStep, userData: $userData)
            case .phoneNumber:
                PhoneNumberStepView(nextStep: $registrationStep, userData: $userData)
            case .verificationCode:
    ...

And then the user data is established in another file as a data model:

class UserData: ObservableObject {
    // Use @Published to automatically update views when these properties change.
    @Published var email: String = ""
    ...

Solution

  • If you need to pass the whole userData model to your EmailRegistrationStepView and PhoneNumberStepView, then try this simple approach, using .environmentObject(userData) as shown in the example code:

    class UserData: ObservableObject {
        @Published var email: String = ""
        @Published var phoneNumber: String = ""
        // ...
    }
    
    // outside RegistrationView
    enum RegistrationStep {
        case emailAndPassword
        case phoneNumber
        // ...
    }
    
    struct RegistrationView: View {
        @State private var registrationStep: RegistrationStep = .emailAndPassword
        @EnvironmentObject var userData: UserData  // <-- here
        
        var body: some View {
            VStack {
                Text(userData.email).foregroundStyle(.red) // <-- to show it works
                
                switch registrationStep {
                case .emailAndPassword:
                    EmailRegistrationStepView(nextStep: $registrationStep) // <-- here
                case .phoneNumber:
                    PhoneNumberStepView(nextStep: $registrationStep)
                // ....
                }
            }
        }
    }
    
    struct EmailRegistrationStepView: View {
        @EnvironmentObject var userData: UserData  // <-- here
        @Binding var nextStep: RegistrationStep  // <-- here, only if you need to change nextStep
      
        var body: some View {
            Text("in EmailRegistrationStepView")
            TextField("email", text: $userData.email).border(.green)  // <-- here, change the email
        }
    }
    
    struct PhoneNumberStepView: View {
        @EnvironmentObject var userData: UserData
        @Binding var nextStep: RegistrationStep
    
        var body: some View {
            Text("PhoneNumberStepView")
            TextField("phoneNumber", text: $userData.phoneNumber)
        }
    }
    
    struct ContentView: View {
        @StateObject var userData = UserData()
        
        var body: some View {
            RegistrationView()
               .environmentObject(userData)  // <-- here
        }
    }
    

    Note, if you put enum RegistrationStep inside the RegistrationView, then you need to use @Binding var nextStep: RegistrationView.RegistrationStep etc... whenever you refer to it.