Introduction

In the previous post: 2 hours to master RxSwift - Part 1 (needone.app) and 2 hours to master RxSwift - Part 2 (needone.app), we get to know the the core concept behind the RxSwift and how RxSwift works. In this tutorial, we are going to combine what we learnt with MVVM.

Section 1: MVVM

Model-View-ViewModel is a software architectural pattern that is commonly used in the mobile development. It isolates the data and views by introducing the intermediary layer named viewModel.

Here is the workflow diagram of the MVVM.

Source: Wikipedia

Model: Model represent the objects we will use. For example, we need to fetch the users from API, then we will create a model user to handle that.

View: We use either SwiftUI or UIKit to represent the UI elements, such as a form, or charts on the screen.

ViewModel: represents the business logic of the application. For example, the api user might a json from remote including firstName, lastName. However, we are required to display the full name in the end. So We need to create a function in the ViewModel to assemble the name.

Notice that ViewModel is dataBinding with View meaning whenever there is a change on either View or ViewModel, a certain action is triggerred to response that. For example, we want to validate user's name (contains invalide characters, username is already taken, etc) when user is typing without hit a button to submit the form. Fortunately, this is quite easy to achieve with the help from RxSwift. We can easily bind the UITextFiled to the ViewModel.

Section 2: Example of using RXSwift in MVVM

As we don't have any API or database call here, so we don't have the Model in the following example.

View:

import UIKit
import RxSwift
import RxCocoa

enum MyError: Error {
  case someError
}

class ViewController: UIViewController {
  
  @IBOutlet weak var emailTextField: UITextField!
  @IBOutlet weak var emailErrorInfo: UILabel!
  
  @IBOutlet weak var mobileTextField: UITextField!
  @IBOutlet weak var mobileErrorInfo: UILabel!
  
  let disposeBag = DisposeBag()
  var viewModel: ViewModel?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    
    viewModel = ViewModel()
    setupBinding()
  }
  
  func setupBinding() {
    emailTextField.rx.text.changed.subscribe(onNext: {
      guard let text = $0, let viewModel = self.viewModel else { return }
      self.emailErrorInfo.text =
      viewModel.validateEmail(email: text) ?
        "" :
        "Invalid character"
    }).disposed(by: disposeBag)
    
    
    mobileTextField.rx.text.changed.subscribe(onNext: {
      guard let text = $0, let viewModel = self.viewModel else { return }
      self.mobileErrorInfo.text =
        viewModel.validateMobile(mobile: text) ?
          "" :
          "Invalid character"
    }).disposed(by: disposeBag)
  }
}

ViewModel

import Foundation

final class ViewModel {
  
  
  func validateEmail(email: String) -> Bool {
    // sophisticated logic to validate the email, here just simply to check "!"
    let emailRegEx = "^(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?(?:(?:(?:[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+(?:\\.[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+)*)|(?:\"(?:(?:(?:(?: )*(?:(?:[!#-Z^-~]|\\[|\\])|(?:\\\\(?:\\t|[ -~]))))+(?: )*)|(?: )+)\"))(?:@)(?:(?:(?:[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(?:\\.[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)*)|(?:\\[(?:(?:(?:(?:(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))\\.){3}(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))))|(?:(?:(?: )*[!-Z^-~])*(?: )*)|(?:[Vv][0-9A-Fa-f]+\\.[-A-Za-z0-9._~!$&'()*+,;=:]+))\\])))(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?$"
    let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
    let result = emailTest.evaluate(with: email)
    return result
  }
  
  func validateMobile(mobile: String) -> Bool {
    // sophisticated logic to validate the mobile, here just simply to check "!"
    let phoneRegex = "^d{10}$"
    let phoneTest = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
    return phoneTest.evaluate(with: mobile)
  }
}

We can easily test our viewModel methods:

func testExample() throws {
  let viewModel = ViewModel()
  XCTAssertTrue(viewModel.validateEmail(email: "1@abc.com"))
  XCTAssertFalse(viewModel.validateMobile(mobile: "12345678900"))
}