MVVM with RxSwift

Maximilian Alexander

 

 

https://goo.gl/stQQDO

View This Live with Me!

What Does It Solve?

MVC Pattern

Massive
View
Controller

Instead of

Shoving EVERYTHING into your ViewController

  • Model

  • View

  • ViewModel

View

  • UIButton
  • UITextField
  • UITableView
  • UICollectionView
  • NSLayoutConstraints

View

class LoginViewController : UIViewController {

    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var confirmButton: UIButton

}

Code So Far

Model

  • CLLocationManager
  • Alamofire
  • Facebook SDK
  • Database like Realm
  • Service Classes

Model

The Model is just an abstract word for
"your data"

ViewModel

1. Prepares Data

2. Manipulates Data

struct LoginViewModel {

    var username: String = ""
    var password: String = ""

    func attemptToLogin() {
        let params = [
            "username": username,
            "password": password
        ]

        ApiClient.shared.login(email: email, password: password) 
            { (response, error) in
            
            }
    }
}
class LoginViewController : UIViewController {

    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var confirmButton: UIButton

    //viewModel is just a member variable here. 
    var viewModel = LoginViewModel()

    override func viewDidLoad(){
        super.viewDidLoad()
    }

}

•UITextField

•UITextField

•UIButton

 

ViewModel

 

 

Model

 

•tapFunction()

•doSomething()

Hooking UI to ViewModel and Back

Not as simple as you think

class LoginViewController : UIViewController {

    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var confirmButton: UIButton

    //viewModel is just a member variable here. 
    var viewModel = LoginViewModel()

    override func viewDidLoad(){
        super.viewDidLoad()
        usernameTextField.addTarget(self, action: 
           #selector(LoginViewController.usernameTextFieldDidChange:_), 
            forControlEvents: UIControlEvents.EditingChanged)
        passwordTextField.addTarget(self, 
            action: #selector(LoginViewController.passworldTextFieldDidChange:_), 
            forControlEvents: UIControlEvents.EditingChanged)

    }

    func usernameTextFieldDidChange(textField: UITextField){
        viewModel.username = textField.text ?? ""
    }

    func passworldTextFieldDidChange(textField: UITextField){
        viewModel.password = textField.text ?? ""
    }

    func confirmButtonTapped(sender: UIButton){
        viewModel.attemptLogin()
    }

}

That looked less fun

What about UI that reacts to changing events? 

Enabled/Disable Confirm Button

aka.

Form Validation 

•UITextField

•UITextField

•UIButton

 

ViewModel

 

 

Model

 

•tapFunction()

•doSomething()

This is Hard

struct LoginViewModel {

    var username: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var password: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var isValid : Bool = ""

    func attemptToLogin() {
        let params = [
            "username": username,
            "password": password
        ]
        ApiClient.shared.login(email: email, password: password) 
            { (response, error) in
            
            }
    }

    private func evaluateValidity(){
      isValid = username.characters.count > 0 
            && password.characters.count > 0
    }
}

Now how do we hookup isValid to confirmButton?

Important!

ViewModel does not hold a reference to the ViewController!

struct LoginViewModel {

    var username: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var password: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var isValid : Bool = "" {
        didSet {
            loginViewController?.confirmButton.isDisabled = !isValid
        }
    }
    weak var loginViewController : LoginViewController?

    func attemptToLogin() {
        //truncated for space
    }

    private func evaluateValidity(){
      isValid = username.characters.count > 0 
            && password.characters.count > 0
    }
}

💀 Never do this!

View Model DONTS

 

  • Reference View Controller
  • Don't import UIKit
  • Reference anything from UIKit
  • Data should be going in and out
struct LoginViewModel {

    var username: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var password: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var isValid : Bool = "" {
        didSet {
            isValidCallback?(isValid: isValid)
        }
    }
    var isValidCallback : ((isValid: Bool) -> Void)?

    func attemptToLogin() {
        //truncated for space
    }

    private func evaluateValidity(){
      isValid = username.characters.count > 0 
            && password.characters.count > 0
    }
}
class LoginViewController {

    @IBOutlet var confirmButton: UIButton!
    var loginViewModel = LoginViewModel()

    override func viewDidLoad(){
        super.viewDidLoad()
        loginViewModel.isValidCallback = { [weak self] (isValid) in
            self?.confirmButton.isEnabled = isValid
        }
    }
}

Listen to the Callback

That's it for MVVM!

Unidirectional Data Flow with MVVM is hard

What we want is something like this:

 

 

UIKit

 

 

 

ViewModel

 

 

Model

 

How can RxSwift help with this?

import RxSwift

struct LoginViewModel {
    
    var username = Variable<String>("")
    var password = Variable<String>("")

    var isValid : Observable<Bool>{
        return Observable.combineLatest( self.username, self.password) 
        { (username, password) in
            return username.characters.count > 0 
                        && password.characters.count > 0
        }
    }

}
import RxSwift
import RxCocoa

class LoginViewController {
    
    var usernameTextField: UITextField!
    var passwordTextField: UITextField!
    var confirmButton: UIBUtton!

    var viewModel = LoginViewModel()

    var disposeBag = DisposeBag()
    
    override func viewDidLoad(){
        super.viewDidLoad()
        usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag)
        passwordTextField.rx.text.bindTo(viewModel.password).addTo(disposeBag)  
        
        //from the viewModel
        viewModel.rx.isValid.map{ $0 }
            .bindTo(confirmButton.rx.isEnabled)
    }

}

That's How Easy It Is!

Make sure the UIBindings Don't Talk to Each Other

class ViewController : UIViewController {

override func viewDidLoad(){
    super.viewDidLoad()

    let isValid Observable.combineLatest(
            username.rx.text, 
            password.rx.text, 
            resultSelector: { (username, password) -> Bool in
                return username.characters.count > 0 
                        && password.characters.count > 0
            })

    isValid.bindTo(confirmButton.rx.isEnabled)
        .addTo(disposeBag)
    }

}

ViewController is still massive 😔

class MyCustomView : UIView {

    var sink : AnyObserver<SomeComplexStructure> {
        return AnyObserver { [weak self] event in
            switch event {
                case .next(let data):
                    self?.something.text = data.property.text
                    break
                case .error(let error):
                    self?.backgroundColor = .red
                case .completed:
                    self.alpha = 0
            }
        }
    }

}

What if RxSwift + RxCocoa doesn't have bindings

class ViewController {

    let myCustomView : MyCustomView

    override func viewDidLoad(){
        super.viewDidLoad()

        viewModel.dataStream
            .bindTo(myCustomView.sink)
            .addTo(disposeBag)
    }
}

Custom Binding Sinks

Looking Forward

pod 'RxSwift'
pod 'RxCocoa'

#the good stuff

pod 'RxDataSource'

MVVM with RxSwift

By Max Alexander

MVVM with RxSwift

  • 2,509