How to use RxSwift with MVVM pattern – Part 2

This is the second post in the 'How to use RxSwift with MVVM' series. In the first part, we set up RxSwift from Cocoapods and checked how to use Variable, 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 text fields 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.

In case you are not familiar with the Friends app, it is an app that you can use to download a list of friends and display them in a table view. You can also add, remove and update friends. I've implemented the application using MVVM architecture, and of course, I wrote the backend with swift using Vapor! If you want to learn basic MVVM without RxSwift, check out my old post MVVM pattern with Swift application.

You can get the source codes for the application from GitHub, remember to check out the RxSwift branch. You can, of course, also follow this post without the source code. But now, let's get down to business!

Add and edit friends with 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 friends using RxSwift

We have a view that you can use to enter a friend's first name, last name and a phone number. First, we define a protocol named FriendViewModel. Then, we will have two view models that conform 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 the friend's information when we open the editing view.

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)
}

As in the first part with deleting a friend, cell tapping is also set up with a function from the rx extension. Now we use the modelSelected function and subscribe to the events that it emits. First, we'll check that the cell type is normal and bind the viewModel with if case let syntax. Then, we'll store the view model to a new ReadOnce object and perform the wanted segue.

ReadOnce to the rescue

ReadOnce is a helper class that makes sure we are not using an old view model when opening the view:

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
    }
}

The value is stored in a private var value. You can only access it using the read function. In case you have read it once, isRead is set to false. The next time you call this, it will return a nil value instead. We always create a new ReadOnce object when we are proceeding to a new view, and this makes it a lot safer. Now the chances of an old value messing up the new view creation are very small.

Then, we can check if we should perform the segue. Inside the shouldPerformSegue, we can check the isRead variable. If it returns false, we can proceed.

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 that the segue is performed, we will set the view model in the in prepareForSegue like this:

    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)
        }
    }

This way, we'll make sure the view controller is always opened with the correct information.

One interesting thing here is the updateFriends observer. When we are returning back to the friend list after we have updated a friend, we want to make sure our list is up-to-date. That is why we will set up an observer to get an event when we have to update friends from the server. We also want to make sure that this observer is released from the memory. It might be a problem if we have an active observable subscription to a view controller that is released from the memory. We can be sure that we have canceled the subscription when the "ONCOMPLETED" is printed in the console. We'll come back to this issue in the FriendViewController code. I'll show you then how we can prevent the memory issues.

Now we know how to open the view to edit a friend. Next, let's take a look how to implement UpdateFriendViewModel!

RxSwift type definitions in FriendViewModel protocol

Take a look at the protocol definition below.

protocol FriendViewModel {
    var title: Variable<String> { get }
    var firstname: Variable<String> { get }
    var lastname: Variable<String> { get }
    var phonenumber: Variable<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 }
}

All the variable definitions are familiar to us by now, but let's do a quick recap. We have Variable, Observable and PublishSubject. As we remember, they are all observables. Observable sends OnNextOnError or OnCompleted to change the state. With Variable, we use the value property to update its value, which is then emitted to the subscriber with an onNext event. Variable is also guaranteed not to emit an error. PublishSubject acts the same as Observable but it can also subscribe to other observables.

I know this was a quick review of the RxSwift observables. In case you want to recap how to subscribe to events etc., please check the first post of the series.  Now, let's move on to the UpdateFriendViewModel to see how we should use all those.

UpdateFriendViewModel implemented with RxSwift

As said, UpdateFriendViewModel conforms to FriendViewModel. Let's check the implementation of the variables first:

final class UpdateFriendViewModel: FriendViewModel {
    let onShowError = PublishSubject<SingleButtonAlert>()
    let onNavigateBack = PublishSubject<Void>()
    let submitButtonTapped = PublishSubject<Void>()
    let disposeBag = DisposeBag()

    var title = Variable<String>("Update Friend")
    var firstname = Variable<String>("")
    var lastname = Variable<String>("")
    var phonenumber = Variable<String>("")
    var onShowLoadingHud: Observable<Bool> {
        return loadInProgress
            .asObservable()
            .distinctUntilChanged()
    }

    var submitButtonEnabled: Observable<Bool> {
        return Observable.combineLatest(firstnameValid, lastnameValid, phoneNumberValid) { $0 && $1 && $2 }
    }

    private let loadInProgress = Variable(false)

At the top we have PublishSubjects. They emit events to show error and navigate back to the friend list view when users tap the submit button. Below those, we have the good old disposeBag. Then we have all the friends information defined as Variables. We also have the title for the view: "Update Friend". The last two public variables are onShowLoadingHud and submitButtonEnabled.

We want the loadInProgress to be a private variable. This way we can't change the state in the view controller side. Instead, we have defined onShowLoadingHud as a computed property. This is a public observable we can use in the view controller side. It returns the loadInProgress as an observable. We subscribe to this observable in the view controller side and get notified when it changes its state. distinctUntilChanged makes sure the value is only sent once.

SubmitButtonEnabled is a bit more interesting. It combines a few variables and validates the inputs. It then decides if the button should be enabled or not.

Validating input with RxSwift

To do this, we have to introduce a few more private variables:

private var firstnameValid: Observable {
    return firstname.asObservable().map { $0.count > 0 }
}
private var lastnameValid: Observable {
    return lastname.asObservable().map { $0.count > 0 }
}
private var phoneNumberValid: Observable {
    return phonenumber.asObservable().map { $0.count > 0 }
}

We have a computed variable for every input the user can edit. All the variables return the original variable as an observable. For example: we tie firstnameValid with firstname. After the asObservable, we use map and check if the value in the variable is longer than zero characters and return a boolean.

Now, let's look at the submitButtonEnabled observable again. We can see that it is observing all those private variables we defined. It takes the latest values emitted and combines the boolean values. If all are true, the button is then enabled and the user can update the information to the backend.

The last two variables in the view model are:

private let appServerClient: AppServerClient
private let friendId: Int

AppServerClient is the layer for all the networking in the app. When a user updates a friend's information to the backend, we use appServerClient. We need the last variable friendId to identify the friend that the user is updating.

UpdateFriendViewModel constructor and submitFriend function

We are almost done with the view model. Next, we will get into the constructor and sending friend information. Let's start with the constructor:

init(friendCellViewModel: FriendCellViewModel, appServerClient: AppServerClient = AppServerClient()) {
        self.firstname.value = friendCellViewModel.firstname
        self.lastname.value = friendCellViewModel.lastname
        self.phonenumber.value = friendCellViewModel.phonenumber
        self.friendId = friendCellViewModel.id
        self.appServerClient = appServerClient

        self.submitButtonTapped.asObserver()
            .subscribe(onNext: { [weak self] in
                self?.submitFriend()
                }
        ).disposed(by: disposeBag)
    }

Inside the constructor, we first set all the friend information that we get from the FriendCellViewModel. Then we'll set appServerClient. We want to use dependency injection here to make the class testable. And lastly, we will set up the submitButton. We subscribe to the onNext event and call submitFriend when the user clicks the button.

Submit friend information to the server

Submitting friend information is done like this:

private func submitFriend() {
    loadInProgress.value = true

    appServerClient.patchFriend(
        firstname: firstname.value,
        lastname: lastname.value,
        phonenumber: phonenumber.value,
        id: friendId)
        .subscribe(
            onNext: { [weak self] friend in
                self?.loadInProgress.value = false
                self?.onNavigateBack.onNext(())
            },
            onError: { [weak self] error in
                self?.loadInProgress.value = 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)
}

First, we'll set loadInProgress to true to active the loading indicator. Next, we'll call the patchFriend function from the AppServerClient. It takes the friend information as a parameter and returns an Observable we can subscribe to. When the request is a success, the observable emits an onNext event. Then we'll hide the loading indicator and emit an onNavigateBack event. This way, we'll return to the friend list view.

If something went wrong, the observable emits an onError event. We'll hide the loading indicator, and create a SingleButtonAlert with the correct title, message and action. Since the alert is dismissed automatically after button press, the only action here is to print a text in the console to see that the button press works. After that, we'll emit an onNext event for the onShowError to present the alert. One thing you might be wondering here is the getErrorMessage function. At the bottom of the UpdateFriendViewModel, we have defined a private extension for PatchFriendFailureReason. We use it to convert known error messages to text presented to the user:

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."
        }
    }
}

It checks if the PatchFriendFailureReason enum contains a known value and returns a text we can present in the error popup.

Now we'll move on to the view controller side!

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 the view model, etc.

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()

    let disposeBag = DisposeBag()

    private var activeTextField: UITextField?

Here we also see the updateFriends variable which we already talked about. With that, 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:

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:

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)
    ...
}

When the view is loaded, we'll call the bindViewModel function. Inside, we'll make sure we subscribe to all events that change the UI state. Furthermore, we also make sure that we update all values to the view model that the user can change in the UI. First, we'll unwrap the viewModel to get rid of all the optional handling. Down the road, this makes the code a lot cleaner. Next, we'll set the title for the view. title = viewModel.title.value. Then we have a bit more interesting things to do.

Binding UITextField to Variable and back again

The text fields and the friend values in the view model need to be bound both ways. When the view controller is opened, we want to fill the text fields in the form with the values from the view model. Also, when the user changes those values, we want to update the new values to the view model. To do this, we make a private function: bind(textField: UITextField, to variable: Variable)

private func bind(textField: UITextField, to variable: Variable) {
    variable.asObservable()
        .bind(to: textField.rx.text)
        .disposed(by: disposeBag)
    textField.rx.text.unwrap()
        .bind(to: variable)
        .disposed(by: disposeBag)
}

Variable is the variable we have on the view model side. We'll bind that value to the text field by using the text property from the rx extension. This way we always update the text field when we open the view for the first time.

With textField, we'll also use the text variable from the rx extension. Then, we'll use unwrap functions since the text variable is an optional (to make sure it actually contains a value). And finally, we bind it to the view model's variable. We also add both of the parameters to disposeBag to make sure we won't have any memory issues.

This is a lot cleaner solution. It saves us a lot of lines compared to calling those to all text fields and view model variables separately. I really like it, I hope you do, too!

Continuing with the bindViewModel

Next, let's activate and bind the submit button and the present loading hud and errors to the user.

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
        .asObservable()
        .map { [weak self] in self?.setLoadingHud(visible: $0) }
        .subscribe()
        .disposed(by: disposeBag)

    viewModel
        .onNavigateBack
        .asObservable()
        .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)
}

Again, we are using the rx extension. We bind view models submitButtonEnabled to the buttonSubmit and also set a tap handler to the view models submitButtonTapped.

Next, we'll subscribe to onShowLoadingHud. Again, we use map to get the boolean value from the event that it emits. And call for the setLoadingHud to present or hide the loading hud.

private func setLoadingHud(visible: Bool) {
    PKHUD.sharedHUD.contentView = PKHUDSystemActivityIndicatorView()
    visible ? PKHUD.sharedHUD.show(onView: view) : PKHUD.sharedHUD.hide()
}

onNavigateBack is pretty straightforward. The only thing we need to remember here is to emit the updateFriends event to update the friend list. With onShowError, we once again use map to get the boolean value. Then we call the presentSingleButtonDialog to display the error.

The rest of the code is just about handling the activation of the text fields. In case you want to know how it is done, please refer to the MVVM with Swift application part 3 and search for activeTextField.

Conclusion

And that is all that I wanted to go through with you today! As said, I won't go through the AddFriendViewModel in this post. It is so similar to the UpdateFriendViewModel that you can do it by yourself.

With this and the previous post, we have now gone through the basics of RxSwift. We know how to work with the UITableView. How to handle subscriptions to observables back and forth between the view model and the view controller. We also went through the basic validation of user input data and more! However, we still have some work to do. Next time, we'll check how to unit test the application!

I hope you have liked the series so far! In case you have any questions or comments, please post a comment or message me on Twitter! If you liked the post, please spread the word and tell your friends about it. So until next time, have a great day my friend!

Jussi Suojanen

Jussi Suojanen
Swifty SW Developer.

Join the conversation