Cícero Camargo

An Introduction to Combine - Part 2

Welcome back to the second part of this introduction to Combine. In part 1 we learned how to see our applications as a mesh of objects that produce and consume events from each other, and from that point we gave our first step to refactor our mini MVC app using Combine.

In this second part I want to we're going to continue the refactor moving our component towards the MVVM (Model-View-ViewModel) pattern, and by doing that we're gonna learn how we can use Combine to observe updates in stored values.

Refactoring to MVVM

Before we start changing the code, let's talk about the role of the ViewModel. Comparing our component to a living organism, I see the ViewModel as the brain, where the the View/ViewController is the body. The role of the brain is to tell the body how to behave and how it feels at the moment, while the body reacts to that state of the brain and also sends signals, as this being interacts with the environment, for the brain to process. Also, if the organism is too simple there's no need for a complex brain, a pretty dumb one is enough for the body to fill its role in the environment.

Translating this into developer words, the View/ViewController is the body and the ViewModel is the brain. If the component is too dumb, one without any complex/asynchronous behavior, the ViewModel is the state that feeds the view and can be modeled as a value type (struct or enum). Now, if the component contains behavior (which is the case for our Balance component) the ViewModel will encapsulate this behavior by (1) providing an observable state for the View/ViewController to render and (2) handling any events that the View/ViewController might send. The role of the view layer is just binding correctly to the ViewModel, so that its always sends the right events and renders the ViewModel's current state.

So we'll start by defining a class that will own the state of our BalanceViewController and then we'll move all the behavior and state updates into this class.

import Combine
import Foundation
import UIKit

final class BalanceViewModel {
    private(set) var state = BalanceViewState()
    private let service: BalanceService
    private var cancellables: Set<AnyCancellable> = []

    init(service: BalanceService) {
        self.service = service

        NotificationCenter.default
            .publisher(for: UIApplication.willResignActiveNotification)
            .sink { [weak self] _ in
                self?.state.isRedacted = true
            }
            .store(in: &cancellables)

        NotificationCenter.default
            .publisher(for: UIApplication.didBecomeActiveNotification)
            .sink { [weak self] _ in
                self?.state.isRedacted = false
            }
            .store(in: &cancellables)
    }

    func refreshBalance() {
        state.didFail = false
        state.isRefreshing = true
        service.refreshBalance { [weak self] result in
            self?.handleResult(result)
        }
    }

    private func handleResult(_ result: Result<BalanceResponse, Error>) {
        state.isRefreshing = false
        do {
            state.lastResponse = try result.get()
        } catch {
            state.didFail = true
        }
    }
}

You may have wrinkled your nose to that import UIKit at the top because I'm adding a framework dependency to my ViewModel and that will make it harder to reuse this component in that WatchOS app that some customers have been asking for... Yes, I could have done this is a number of ways but I don't want to overcomplicate things here and I really wanted to extract these subscriptions from BalanceViewController, so that in the future we can replace it with a SwfitUI view with minimal effort, so bear with me.

Let's return to our BalanceViewController, which is completely broken, at this point. After some adjustments, this is what I got:

class BalanceViewController: UIViewController {
    private let rootView = BalanceView()
    private let viewModel: BalanceViewModel
    private let formatDate: (Date) -> String
    private var cancellables: Set<AnyCancellable> = []
    
    init(
        service: BalanceService,
        formatDate: @escaping (Date) -> String = BalanceViewState.relativeDateFormatter.string(from:)
    ) {
        self.viewModel = .init(service: service)
        self.formatDate = formatDate
        super.init(nibName: nil, bundle: nil)
    }

    (...)    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        rootView.refreshButton.touchUpInsidePublisher
            .sink(receiveValue: viewModel.refreshBalance)
            .store(in: &cancellables)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        viewModel.refreshBalance()
    }
    
    private func updateView() {
        rootView.refreshButton.isHidden = viewModel.state.isRefreshing
        if viewModel.state.isRefreshing {
            rootView.activityIndicator.startAnimating()
        } else {
            rootView.activityIndicator.stopAnimating()
        }
        rootView.valueLabel.text = viewModel.state.formattedBalance
        rootView.valueLabel.alpha = viewModel.state.isRedacted
            ? BalanceView.alphaForRedactedValueLabel
            : 1
        rootView.infoLabel.text = viewModel.state.infoText(formatDate: formatDate)
        rootView.infoLabel.textColor = viewModel.state.infoColor
        rootView.redactedOverlay.isHidden = !viewModel.state.isRedacted
    }
}

Looks much shorter now, right? And pay attention to how we simplified that sink call on viewDidLoad(). As the receiveValue closure has a Void input argument and func refreshBalance() also receives no arguments, I can forward the event directly to the ViewModel, there's no need to go through self anymore. Just be careful when using this as it will keep a strong reference from the cancellable to the viewModel, which is fine as long as the viewModel doesn't have a strong reference back to the ViewController, which will ultimately own the cancellable.

Let's run our tests again and... oops! Almost all off them failed. That's because updateView() is not being called anymore when the state is updated by the ViewModel. How could the ViewController know about that?

Maybe the ViewController could set up a closure for the ViewModel to call back every time the state changes, or maybe we could have a delegate protocol between them, or maybe KVO... No, no, no. All of them would work, but we're here to learn Combine. Not just because it's the subject of the article, but because it's better for a numbebr of reasons: we don't need to have weak references, we can have multiple subscribers, we unlock a lot of useful operators, ViewModel doesn't need to inherit from NSObject...

So, what our ViewController needs is that the ViewModel provides a publisher that will send events back every time the state changes. So, based on what we learned in Part 1, you may have thought about this:

final class BalanceViewModel {
    private let stateSubject = PassthroughSubject<BalanceViewState, Never>()
    private(set) var state = BalanceViewState() {
        didSet {
            stateSubject.send(state)
        }
    }
    var statePublisher: AnyPublisher<BalanceViewState, Never> {
        stateSubject.eraseToAnyPublisher()
    }
    
    (...)

Ok, this will work for our use case as the state is updated right after the component appears. If this wasn't the case, the ViewController would have to make an additional call, along with the subscription, to render the initial state. This is not ideal, so let me introduce CurrentValueSubject.

CurrentValueSubject

This is a subject that stores a value and also publishes changes on it, so that subscribers get the current value right away when they subscribe and also any subsequent updates. Let's see how we could fix our current issue using a CurrentValueSubject.

final class BalanceViewModel {
    private let stateSubject: CurrentValueSubject<BalanceViewState, Never>
    private(set) var state: BalanceViewState {
        get { stateSubject.value }
        set { stateSubject.send(newValue) }
    }
    var statePublisher: AnyPublisher<BalanceViewState, Never> {
        stateSubject.eraseToAnyPublisher()
    }
    
    (...)
    
    init(service: BalanceService) {
        self.service = service
        stateSubject = .init(BalanceViewState())
        
        (...)
    }
    
    (...)
}

Since stateSubject already stores a BalanceViewState it has become our source of truth, and state has become just a proxy to get the current value from stateSubject and push changes to stateSubject through a send call. Just be careful that everytime we change any property under BalanceViewState, a new state is published to the subscribers. This is happenning in our code as we change the state properties in place, one by one, instead of making a copy of the state, changing all the properties we need, and then writing the new value back to self.state again. It's not a big deal for our simple example, just be aware of this.

Also, similarly to what we've done in our CustomButton, we don't want to expose the subject itself because only BalanceViewModel should be able to call send on it, so we're providing it to the external world as an AnyPublisher.

Now back to BalanceViewController, all we need to do is adding the following after super.viewDidLoad():

viewModel.statePublisher
    .sink { [weak self] _ in self?.updateView() }
    .store(in: &cancellables)

We can just ignore the input value for now because we are already accessing it directly in the viewModel inside func updateView().

It's time to run our tests again. They all pass! Yay! You can see the full diff here.

@Published

Once you start using CurrentValueSubject you'll find yourself repeating the same pattern again and again: the subject is the source of truth, you have a var to access it in a more convenient way, and you also have to expose it to subscribes a read-only publisher. Maybe we could write a property-wrapper to encapsulate all this...

I have good news: it already exists. If you have played with SwiftUI you might also have used the @Published property-wrapper. Let's see how we can use it in our UIKit app.

All we have to do in BalanceViewModel is replacing those 3 members (stateSubject, statePublisher and that proxy state var) with:

@Published private(set) var state = BalanceViewState()

It always reads a bit funny to me because it's "published" (which sounds like it's public) but it's also "private" at the same time. Anyway, this is exactly what we need: a value that can only be written by it's owner (BalanceViewModel) but can be read and observed from the outside.

Now we have to adjust the ViewController. All we need to do is changing where we were accessing viewModel.statePublisher to viewModel.$state that we'll have access to the publisher that comes with this property wrapper.

We run our tests again and... they fail! WTF?!

Well, we are ignoring the value that comes from the publisher and reaching the ViewModel again to get the current state. The problem is that @Published publishes the new value on the willSet of the property it wraps, so at the time the subscribers receive the new value the source of truth hasn't been written yet. This works seamlessly in SwiftUI because the framework does all the magic and recalculates the View's body at the right moment, but in our case if we want to use @Published we can't ignore the value, so let's update our BalanceViewController and start using the state from the publisher instead of reaching the viewModel again on updateView:

class BalanceViewController: UIViewController {
    (...)
    
    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.$state
            .sink { [weak self] in self?.updateView(state: $0) }
            .store(in: &cancellables)
        
        (...)
    }
    
    (...)
    
    private func updateView(state: BalanceViewState) {
        rootView.refreshButton.isHidden = state.isRefreshing
        if state.isRefreshing {
            rootView.activityIndicator.startAnimating()
        } else {
            rootView.activityIndicator.stopAnimating()
        }
        rootView.valueLabel.text = state.formattedBalance
        rootView.valueLabel.alpha = state.isRedacted
            ? BalanceView.alphaForRedactedValueLabel
            : 1
        rootView.infoLabel.text = state.infoText(formatDate: formatDate)
        rootView.infoLabel.textColor = state.infoColor
        rootView.redactedOverlay.isHidden = !state.isRedacted
    }
}

And our tests are back to green. Here's the full change.

Conclusion

Being able to observe changes in stored properties is a pre-requisite for applying the MVVM pattern and Combine helps a lot with this task, specially with the @Published property wrapper. But we have to be aware of its details, so don't be like me please read the documentation.

In part 3, we'll continue this refactor and learn new ways of creating subscriptions.

I'm also pushing this series of articles into the README of the repo, so you can start watching the repo and follow me on Twitter or LinkedIn to be notified about updates.

If you have suggestions, corrections, etc., feel free to open a Pull Request or send me a message.

See you soon.

Tagged with: