
How to use RxSwift with MVVM pattern part 2
This is the second post on how to use RxSwift with MVVM series. In the first part we set up RxSwift from Cocoa pods and checked how to use BehaviorRelay, Observable and PublishSubject. This time we will create a view that we can use to create and update friends to the server. We will also see how to validate the inputs in all of the textfields before activating the submit button. After, that we will check how to bind data back and forth in UITextField, between the view model and the view.
Creating and updating a friend using RxSwift
So, our goal is to add friend information to the server, and also to update that information. We’ll do that using the same view.

Add and edit friend RxSwift
We have a view that you can use to enter the first name, last name and the phone number of a friend. First, we define a protocol named FriendViewModel. Then, we will have two view models that conforms to that protocol: AddFriendViewModel & UpdateFriendViewModel. In this tutorial we’ll dive into UpdateFriendViewModel. It does all the same things as AddFriendViewModel and fills the text fields with friends information when we open the view to edit friends information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private func setupCellTapHandling() { tableView .rx .modelSelected(FriendTableViewCellType.self) .subscribe( onNext: { [weak self] friendCellType in if case let .normal(viewModel) = friendCellType { self?.selectFriendPayload = ReadOnce(viewModel) self?.performSegue(withIdentifier: "friendToUpdateFriend", sender: self) } if let selectedRowIndexPath = self?.tableView.indexPathForSelectedRow { self?.tableView?.deselectRow(at: selectedRowIndexPath, animated: true) } } ) .disposed(by: disposeBag) } |
ReadOnce to the rescue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class ReadOnce<Value> { var isRead: Bool { return value == nil } private var value: Value? init(_ value: Value?) { self.value = value } func read() -> Value? { defer { value = nil } if value != nil { return value } return nil } } |
1 2 3 4 5 6 7 |
public override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "friendToUpdateFriend" { return !selectFriendPayload.isRead } return super.shouldPerformSegue(withIdentifier: identifier, sender: sender) } |
Now that we know segue is performed, we will set the view model in the in prepareForSegue like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "friendToUpdateFriend", let destinationViewController = segue.destination as? FriendViewController, let viewModel = selectFriendPayload.read() { destinationViewController.viewModel = UpdateFriendViewModel(friendCellViewModel: viewModel) destinationViewController.updateFriends.asObserver().subscribe(onNext: { [weak self] () in self?.viewModel.getFriends() }, onCompleted: { print("ONCOMPLETED") }).disposed(by: destinationViewController.disposeBag) } } |
RxSwift type definitions in FriendViewModel protocol
Take a look at the protocol defined below.
1 2 3 4 5 6 7 8 9 10 11 |
protocol FriendViewModel { var title: BehaviorRelay<String> { get } var firstname: BehaviorRelay<String> { get } var lastname: BehaviorRelay<String> { get } var phonenumber: BehaviorRelay<String> { get } var submitButtonTapped: PublishSubject<Void> { get } var onShowLoadingHud: Observable<Bool> { get } var submitButtonEnabled: Observable<Bool> { get } var onNavigateBack: PublishSubject<Void> { get } var onShowError: PublishSubject<SingleButtonAlert> { get } } |
UpdateFriendViewModel implemented with RxSwift
As said, UpdateFriendViewModel conforms to FriendViewModel. Let’s check the implementation of the variables first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import RxSwift import RxCocoa final class UpdateFriendViewModel: FriendViewModel { let onShowError = PublishSubject<SingleButtonAlert>() let onNavigateBack = PublishSubject<Void>() let submitButtonTapped = PublishSubject<Void>() let disposeBag = DisposeBag() var title = BehaviorRelay<String>(value: "Update Friend") var firstname = BehaviorRelay<String>(value: "") var lastname = BehaviorRelay<String>(value: "") var phonenumber = BehaviorRelay<String>(value: "") var onShowLoadingHud: Observable<Bool> { return loadInProgress .asObservable() .distinctUntilChanged() } var submitButtonEnabled: Observable<Bool> { return Observable.combineLatest(firstnameValid, lastnameValid, phoneNumberValid) { $0 && $1 && $2 } } private let loadInProgress = BehaviorRelay(value: false) |
Validating input with RxSwift
To do this we have to introduce a few more private variables:
1 2 3 4 5 6 7 8 9 |
private var firstnameValid: Observable<Bool> { return firstname.asObservable().map { $0.count > 0 } } private var lastnameValid: Observable<Bool> { return lastname.asObservable().map { $0.count > 0 } } private var phoneNumberValid: Observable<Bool> { return phonenumber.asObservable().map { $0.count > 0 } } |
1 2 |
private let appServerClient: AppServerClient private let friendId: Int |
UpdateFriendViewModel constructor and submitFriend function
1 2 3 4 5 6 7 8 9 10 11 12 13 |
init(friendCellViewModel: FriendCellViewModel, appServerClient: AppServerClient = AppServerClient()) { self.firstname.accept(friendCellViewModel.firstname) self.lastname.accept(friendCellViewModel.lastname) self.phonenumber.accept(friendCellViewModel.phonenumber) self.friendId = friendCellViewModel.id self.appServerClient = appServerClient self.submitButtonTapped.asObserver() .subscribe(onNext: { [weak self] in self?.submitFriend() } ).disposed(by: disposeBag) } |
Submit friend information to the server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
private func submitFriend() { loadInProgress.accept(true) appServerClient.patchFriend( firstname: firstname.value, lastname: lastname.value, phonenumber: phonenumber.value, id: friendId) .subscribe( onNext: { [weak self] friend in self?.loadInProgress.accept(false) self?.onNavigateBack.onNext(()) }, onError: { [weak self] error in self?.loadInProgress.accept(false) let okAlert = SingleButtonAlert( title: (error as? AppServerClient.PatchFriendFailureReason)?.getErrorMessage() ?? "Could not connect to server. Check your network and try again later.", message: "Failed to update information.", action: AlertAction(buttonTitle: "OK", handler: { print("Ok pressed!") }) ) self?.onShowError.onNext(okAlert) } ) .disposed(by: disposeBag) } |
1 2 3 4 5 6 7 8 9 10 |
fileprivate extension AppServerClient.PatchFriendFailureReason { func getErrorMessage() -> String? { switch self { case .unAuthorized: return "Please login to update friends friends." case .notFound: return "Failed to update friend. Please try again." } } } |
RxSwift with MVVM FriendViewController
We use FriendViewController to create a new friend and also to update an old one. At the top of the file, we have familiar definitions for UI components and view model etc.
1 2 3 4 5 6 7 8 9 10 11 12 |
final class FriendViewController: UIViewController { @IBOutlet weak var textFieldFirstname: UITextField! @IBOutlet weak var textFieldLastname: UITextField! @IBOutlet weak var textFieldPhoneNumber: UITextField! @IBOutlet weak var buttonSubmit: UIButton! var viewModel: FriendViewModel? var updateFriends = PublishSubject<Void>() let disposeBag = DisposeBag() private var activeTextField: UITextField? |
Here we also see the updateFriends variable which we already talked about. With this, we’ll inform the friend list to update itself. As we discussed, this can lead to memory issues. We want to make sure that we release the observer from the memory when the view controller is released. We can make sure this happens by calling onCompleted for the observer in the viewWillDisapper:
1 2 3 4 5 |
override func viewWillDisappear(_ animated: Bool) { updateFriends.onCompleted() super.viewWillDisappear(animated) } |
Binding view model values
Now, let’s check how we handle the binding between the view model and the controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
override func viewDidLoad() { super.viewDidLoad() bindViewModel() } func bindViewModel() { guard let viewModel = viewModel else { return } title = viewModel.title.value bind(textField: textFieldFirstname, to: viewModel.firstname) bind(textField: textFieldLastname, to: viewModel.lastname) bind(textField: textFieldPhoneNumber, to: viewModel.phonenumber) ... } |
Binding UITextField to BehaviorRelay and back again
1 2 3 4 5 6 7 8 |
private func bind(textField: UITextField, to behaviorRelay: BehaviorRelay<String>) { behaviorRelay.asObservable() .bind(to: textField.rx.text) .disposed(by: disposeBag) textField.rx.text.unwrap() .bind(to: behaviorRelay) .disposed(by: disposeBag) } |
Continuing with the bindViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
func bindViewModel() { .... viewModel.submitButtonEnabled .bind(to: buttonSubmit.rx.isEnabled) .disposed(by: disposeBag) buttonSubmit.rx.tap.asObservable() .bind(to: viewModel.submitButtonTapped) .disposed(by: disposeBag) viewModel .onShowLoadingHud .map { [weak self] in self?.setLoadingHud(visible: $0) } .subscribe() .disposed(by: disposeBag) viewModel .onNavigateBack .subscribe( onNext: { [weak self] in self?.updateFriends.onNext(()) let _ = self?.navigationController?.popViewController(animated: true) } ).disposed(by: disposeBag) viewModel .onShowError .map { [weak self] in self?.presentSingleButtonDialog(alert: $0)} .subscribe() .disposed(by: disposeBag) } |
1 2 3 4 |
private func setLoadingHud(visible: Bool) { PKHUD.sharedHUD.contentView = PKHUDSystemActivityIndicatorView() visible ? PKHUD.sharedHUD.show(onView: view) : PKHUD.sharedHUD.hide() } |
Conclusion
Thank you so much for the tutorial. it helped me a lot to understand the concepts of MVVM and Rxswift
Thanks Pranalee,
Makes my day to hear that I could help! 🙂