Search code examples
xcodemvvmswiftuiproperty-wrapper-published

SwiftUI, How to publish data from view to a viewModel then to a second view?


I have one view (with a Form), a viewModel, and a second view that I hope to display inputs in the Form of the first view. I thought property wrapping birthdate with @Published in the viewModel would pull the Form input, but so far I can't get the second view to read the birthdate user selects in the Form of the first view.

Here is my code for my first view:

struct ProfileFormView: View {
@EnvironmentObject var appViewModel: AppViewModel
@State var birthdate = Date()

var body: some View {
    NavigationView {
        Form {
            Section(header: Text("Personal Information")) {
                DatePicker("Birthdate", selection: $birthdate, displayedComponents: .date)
            }
        }
    }

Here is my viewModel code:

class AppViewModel: ObservableObject {
@Published var birthdate = Date()

func calcAge(birthdate: String) -> Int {
    let dateFormater = DateFormatter()
    dateFormater.dateFormat = "MM/dd/yyyy"
    let birthdayDate = dateFormater.date(from: birthdate)
    let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
    let now = Date()
    let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
    let age = calcAge.year
    return age!

and here is my second view code:

struct UserDataView: View {
@EnvironmentObject var viewModel: AppViewModel
@StateObject var vm = AppViewModel()
var body: some View {
    VStack {
        Text("\(vm.birthdate)")

        Text("You are signed in")
        
        Button(action: {
            viewModel.signOut()
        }, label: {
            Text("Sign Out")
                .frame(width: 200, height: 50)
                .foregroundColor(Color.blue)
        })
    }
}

And it may not matter, but here is my contentView where I can tab between the two views:

struct ContentView: View {
@EnvironmentObject var viewModel: AppViewModel

var body: some View {
    NavigationView {
        ZStack {
            if viewModel.signedIn {
                ZStack {
                    Color.blue.ignoresSafeArea()
                        .navigationBarHidden(true)
                    TabView {
                        ProfileFormView()
                            .tabItem {
                                Image(systemName: "square.and.pencil")
                                Text("Profile")
                            }
                        UserDataView()
                            .tabItem {
                                Image(systemName: "house")
                                Text("Home")
                            }
                    }
                }
            }
            else
            {
                SignInView()
            }
        }
    }
    .onAppear {
        viewModel.signedIn = viewModel.isSignedIn
    }
}

One last note, I've got a second project that requires this functionality (view to viewmodel to view) so skipping the viewmodel and going direct from view to view will not help.

Thank you so much!!


Solution

  • Using a class AppViewModel: ObservableObject like you do is the appropriate way to "pass" the data around your app views. However, there are a few glitches in your code.

    In your first view (ProfileFormView), remove @State var birthdate = Date() and use DatePicker("Birthdate", selection: $appViewModel.birthdate, ....

    Also remove @StateObject var vm = AppViewModel() in your second view (UserDataView), you already have a @EnvironmentObject var viewModel: AppViewModel, no need for 2 of them.

    Put @StateObject var vm = AppViewModel() up in your hierarchy of views, and pass it down (as you do) using the @EnvironmentObject with .environmentObject(vm)

    Read this info to understand how to manage your data: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app