alionthego
alionthego

Reputation: 9773

How to bind a view property to a ViewModel property in SwiftUI

I am having trouble figuring out how to dismiss a view that was presented fullscreen in SwiftUI.

On the sign in page, there is a tap to reset password option. When the user taps there, another view appears. The new view has a different ViewModel than that of the SignInView.

After an asynchronous operation to reset the password, I would like to dismiss the reset password view and go back to the SignInView. An abbreviated version looks like this:

// SignInView

@State var isActive = false

var body: some View {
    Text("Forgot Password?  Tap to reset")
        .onTapGesture {
            isActive = true
        }
        .fullScreenCover(isPresented: $isActive, content: {
            ResetPasswordView(viewModel: ResetPasswordViewModel(), isActive: $isActive)
        })
}


// ResetPasswordView

@ObservedObject var viewModel: ResetPasswordViewModel

@Binding var isActive: Bool

init(viewModel: ResetPasswordViewModel, isActive: Binding<Bool>) {
    self.viewModel = viewModel
    self._isActive = isActive
}

var body: some View {
    Button(action: {
        viewModel.resetPassword()
    }, label: {
        Text("Done")
    })
}

Because the asynchronous password reset operation takes place in the ResetPasswordView's ViewModel, I want to be able to set the isActive to false once it's done and dismiss the ResetPasswordView. But I don't know how to change the isActive from the ViewModel as it was passed directly to the view.

I'm very new to MVVM and not sure I am thinking about this the correct way.

Upvotes: 1

Views: 2762

Answers (2)

noveleven
noveleven

Reputation: 637

Don't bind view's property to viewmodel, viewmodel should manage the view all property state to publish to the view.

// SignInView
@ObservedObject var viewModel = CurrentPageViewModel()

var body: some View {
    Text("Forgot Password?  Tap to reset")
        .onTapGesture {
            viewModel.push()
        }
        .fullScreenCover(isPresented: $viewModel.isActive, content: {
            ResetPasswordView()
        })
}

class CurrentPageViewModel: ObservableObject {
  @Published var isActive: Bool = false
  
  func push() {
    isActive = true
  }
}

Upvotes: 0

staticVoidMan
staticVoidMan

Reputation: 20274

You have binded isActive properly. Now you just need to set it to false.
All you need is the following at the appropriate place:

isActive = false

One way is to let the ViewModel handle isActive directly.

Solution

class ResetPasswordViewModel: ObservableObject {
  @Binding var isActive: Bool

  //...other viewModel variables
  
  init(/*...your other params*/ isActive: Binding<Bool>) {
    self._isActive = isActive
  }
  
  func resetPassword() {
    //...your logic
    isActive = false
  }
}

Example (in-use):

struct ContentView: View {
  @State var isActive = false
  
  var body: some View {
    Text("Forgot Password?  Tap to reset")
      .onTapGesture { isActive = true }
      .fullScreenCover(isPresented: $isActive,
               content: {
                ResetPasswordView(viewModel: ResetPasswordViewModel(isActive: $isActive))
               })
  }
}

struct ResetPasswordView: View {
  @ObservedObject var viewModel: ResetPasswordViewModel
  
  var body: some View {
    Button(action: {
      viewModel.resetPassword()
    }, label: {
      Text("Done")
    })
  }
}

Upvotes: 0

Related Questions