Oli Plunkett
Oli Plunkett

Reputation: 87

Reacting to a Signal in MVVM?

While setting up a login view and using Reactive programming for my first time - I am having trouble generating a Signal from my ViewContoller that will alert my observeValues listener in my view with a Bool containing my form validation:

View File

viewModel.outputs.loginSuccess
    .observeValues { [weak self] value in
        print(value)
}

With my current code loginSuccess is firing every time the email or password text fields are changed (I have .addTarget's in my View File that update my MutableProperites in My Model View file). Where I am experiencing trouble is creating a signal for tryLogin that only emits when the login button is pressed, and then maps my form validation ( emailAndPassword.map(isValid) ), which I can respond to in my View File.

Model View File

import ReactiveCocoa
import ReactiveSwift
import Result

public protocol LoginViewModelInputs {

    /// String value of email textfield text
    func emailChanged(_ email: String?)

    /// String value of password textfield text
    func passwordChanged(_ password: String?)

    /// Call when login button is pressed.
    func loginButtonTapped()
}

public protocol LoginViewModelOutputs {

    /// Emits on login.
    var loginSuccess: Signal<(Bool), NoError> { get }
}

public protocol LoginViewModelType {
    var inputs: LoginViewModelInputs { get }
    var outputs: LoginViewModelOutputs { get }
}

public final class LoginViewModel: LoginViewModelType, LoginViewModelInputs,     LoginViewModelOutputs {

    public init() {

        let emailAndPassword = Signal.combineLatest(
            self.emailChangedProperty.signal.skipNil(),
            self.passwordChangedProperty.signal.skipNil()
        )

        let tryLogin = loginButtonTappedProperty.map {
            emailAndPassword.map(isValid)
        }

        self.loginSuccess = tryLogin.value
    }

    fileprivate let emailChangedProperty = MutableProperty<String?>(nil)
    public func emailChanged(_ email: String?) {
        self.emailChangedProperty.value = email
    }
    fileprivate let loginButtonTappedProperty = MutableProperty()
    public func loginButtonTapped() {
        self.loginButtonTappedProperty.value = ()
    }
    fileprivate let passwordChangedProperty = MutableProperty<String?>(nil)
    public func passwordChanged(_ password: String?) {
        self.passwordChangedProperty.value = password
    }

    public let loginSuccess: Signal<(Bool), NoError>

    public var inputs: LoginViewModelInputs { return self }
    public var outputs: LoginViewModelOutputs { return self }
}

func isValid(email: String, password: String) -> Bool {
    return !email.characters.isEmpty && !password.characters.isEmpty
}

Any help is appreciated. I haven't found much good documentation to learn about Signals or Observers, but it's possible I haven't been looking in the right places.

Upvotes: 0

Views: 853

Answers (2)

Sebastian Hojas
Sebastian Hojas

Reputation: 4210

Here is the approach that I would take

  • Enable and disable the loginButton based on the validation of the username and password input
  • Bind login action to button event

Here is the code:

let usernameField: UITextField
let passwordField: UITextField
let loginEnabled: MutableProperty<Bool>
let loginButton: UIButton

loginEnabled <~
    Signal.combineLatest(
        usernameField.reactive.controlEvents(.allEditingEvents),
        passwordField.reactive.controlEvents(.allEditingEvents))
    .map { _ -> Bool in
        return validation()
    }

// only allow button to be pressed when validation succeeds
loginButton.reactive.isEnabled <~ loginEnabled

// login action
let loginAction = Action<(String?,String?),Void,NoError> { (username, password) -> SignalProducer<Void, NoError> in
    // replace with login procedure
    return SignalProducer.empty
}

// bind button event to login action
loginButton.reactive.pressed = CocoaAction(loginAction, input: 
    (usernameField.text, passwordField.text))

Upvotes: 1

MeXx
MeXx

Reputation: 3357

Here is an example of an Input Form built with RAC.

Its a little bit outdated though - everything in this example that starts with rex_ (which was an external extension library for UI Bindings which now is integrated directly into RAC) is now changed to .reactive.

  • UITextField.rex_textSignal is now UITextField.reactive.continuousTextValues
  • UIButton.rex_pressed is now UIButton.reactive.pressed

Instead of saving the credentials to NSUserDefaults in the authenticationAction, you would implement your actual authenticate action.

BTW: you should never save user credentials, especially passwords, to NSUserDefaults. Use the Keychain instead!

The Key-Points are

  • Use MutableProperty in your viewModel to store the current input values
  • Use the .reactive property on interface elements and the binding operator <~ to bind your viewModel to the inputs (viewModel.attribute <~ textField.reactive.continuouseTextValues)
  • Use Action to perform the actual work on demand
  • Bind the Action to a UIControl via CocoaAction (button.reactive.pressed = CocoaAction(viewModel.loginAction))

Hope that helps you!

Upvotes: 1

Related Questions