Before we integrate the Combine framework into the project, I want to briefly revisit the RxSwift and RxCocoa integration. RxSwift is a reactive extension for the Swift language and, as the name suggests, RxCocoa reactifies the Cocoa components you use day in day out.

Open AddLocationViewController.swift and navigate to the viewDidLoad() method. The view controller initializes its view model by passing a driver to the designated initializer of the AddLocationViewModel class. Notice that the view controller accesses the search bar's text observable. Every time the text of the search bar changes, the text observable emits the value of the text property of the search bar. This is convenient and it is what you would expect from a reactive library or framework.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // Initialize View Model
    viewModel = AddLocationViewModel(query: searchBar.rx.text.orEmpty.asDriver())

    ...
}

The functionality the Combine framework offers is much more basic, though. You will notice that we need to write boilerplate code to integrate Combine into the project. Some of the heavy lifting is carried out by the add location view controller and the add location view model. You may be wondering why that is.

The primary goal of RxCocoa is to eliminate boilerplate code as much as possible. How does it do that? Let's take the search bar as an example. RxCocoa creates a wrapper around the UISearchBarDelegate protocol. The implementation is quite daunting if you are unfamiliar with the inner workings of RxSwift and RxCocoa, but the idea is straightforward. RxCocoa sets the delegate of the search bar and emits a value every time the text of the search bar changes. The details are more complex, but that is the gist.

import UIKit
import RxSwift

extension Reactive where Base: UISearchBar {

    public var delegate: DelegateProxy<UISearchBar, UISearchBarDelegate> {
        RxSearchBarDelegateProxy.proxy(for: base)
    }

    public var text: ControlProperty<String?> {
        value
    }

    public var value: ControlProperty<String?> {
        let source: Observable<String?> = Observable.deferred { [weak searchBar = self.base as UISearchBar] () -> Observable<String?> in
            let text = searchBar?.text

            let textDidChange = (searchBar?.rx.delegate.methodInvoked(#selector(UISearchBarDelegate.searchBar(_:textDidChange:))) ?? Observable.empty())
            let didEndEditing = (searchBar?.rx.delegate.methodInvoked(#selector(UISearchBarDelegate.searchBarTextDidEndEditing(_:))) ?? Observable.empty())

            return Observable.merge(textDidChange, didEndEditing)
                    .map { _ in searchBar?.text ?? "" }
                    .startWith(text)
        }

        let bindingObserver = Binder(self.base) { (searchBar, text: String?) in
            searchBar.text = text
        }

        return ControlProperty(values: source, valueSink: bindingObserver)
    }

    ...
}

The Combine framework currently doesn't offer these niceties. Let's hope that changes at some point in the future.

Combine in a Nutshell

The goal of this episode is to show you how Combine can be integrated into the Model-View-ViewModel pattern. I won't cover Combine in much detail. I only explain what is important to understand the bigger picture. If you want learn more about Combine, I recommend taking a look at Building Reactive Applications With Combine in which I explain the Combine framework in depth.

Refactoring the View Model

Let's start by integrating Combine into the project. We start with the AddLocationViewModel class. Open AddLocationViewModel.swift and add an import statement for the Combine framework at the top.

import Combine
import Foundation
import CoreLocation

class AddLocationViewModel {
    ...
}

The next step is updating the query, querying, and locations properties. We start by removing the didSet property observers.

import Combine
import Foundation
import CoreLocation

class AddLocationViewModel {

    // MARK: - Properties

    var query: String = ""

    // MARK: -

    private var querying = false

    // MARK: -

    private var locations: [Location] = []

    ...

}

We modify the access level of the querying property. Only the setter of querying needs to be private.

import Combine
import Foundation
import CoreLocation

class AddLocationViewModel {

    // MARK: - Properties

    var query: String = ""

    // MARK: -

    private(set) var querying = false

    // MARK: -

    private var locations: [Location] = []

    ...

}

To reactify the AddLocationViewModel class, we apply the Published property wrapper to the query and querying properties. The Published property wrapper creates a publisher for the property. The publisher can be accessed through the projected value of the property wrapper. I explain this in more detail in a moment.

import Combine
import Foundation
import CoreLocation

class AddLocationViewModel {

    // MARK: - Properties

    @Published var query: String = ""

    // MARK: -

    @Published private(set) var querying = false

    // MARK: -

    private var locations: [Location] = []

    ...
}

Because we removed the didSet property observers of querying and locations, we can also remove the queryingDidChange and locationsDidChange properties. We no longer need them.

Adding a Current Value Subject

Notice that we don't apply a Published property wrapper to the locations property. The reason is quite technical and I explain this in detail in Building Reactive Applications With Combine. Instead of applying a Published property wrapper to the locations property, we define a constant, private property with name locationsSubject property. We create a current value subject and assign it to the locationsSubject. The Output type of the current value subject is [Location] and its Failure type is Never. The initial value of the current value subject is an empty array.

private var locations: [Location] = []

private let locationsSubject = CurrentValueSubject<[Location], Never>([])

As the name suggests, a current value subject exposes its current value, that is, the last value it published. The current value can be accessed through the value property of the current value subject.

We have two options. We can update the add location view model to use the current value subject or we can update the implementation of the locations property. I want to show you the latter option because I like how it keeps the implementation of the AddLocationViewModel class simple and readable.

The locations property should no longer be a stored property because the array of locations is stored by locationsSubject. We turn locations into a computed property and implement a getter and a setter. The getter returns the value of the value property of locationsSubject and the setter updates the value of the value property of locationsSubject. This simple change means that we don't need to change how we use locations in the AddLocationViewModel class.

private var locations: [Location] {
    get {
        locationsSubject.value
    }
    set {
        locationsSubject.value = newValue
    }
}

private let locationsSubject = CurrentValueSubject<[Location], Never>([])

We need to expose locationsSubject to the add location view controller. We could remove the private keyword, but that isn't what I have in mind. If we take that approach, the add location view controller has direct access to the value property of the current value subject. That isn't what we want.

The solution is simple. We create a computed property, locationsPublisher, of type AnyPublisher. The Output and Failure types of locationsPublisher are identical to those of locationsSubject. In the body of the computed property, we invoke eraseToAnyPublisher() on locationsSubject and return the result.

private var locations: [Location] {
    locationsSubject.value
}

var locationsPublisher: AnyPublisher<[Location], Never> {
    locationsSubject.eraseToAnyPublisher()
}

private let locationsSubject = CurrentValueSubject<[Location], Never>([])

We are required to invoke eraseToAnyPublisher() to hide the actual type of locationsSubject. This is a common pattern when using Combine. I explain this in detail in Building Reactive Applications With Combine.

Attaching a Subscriber

We create a private helper method, setupBindings(), in which the view model subscribes to the query publisher.

// MARK: - Helper Methods

private func setupBindings() {

}

We invoke setupBindings() in the initializer of the AddLocationViewModel class.

// MARK: - Initialization

init() {
    // Setup Bindings
    setupBindings()
}

Let's implement the setupBindings() method. Because we applied the Published property wrapper to the query property, the view model has access to a publisher to which it can subscribe. We access the publisher of the query property by prefixing the property name with a dollar sign, $. The view model attaches a subscriber by invoking the sink(receiveValue:) method. In the closure the view model passes to the sink(receiveValue:) method, it invokes the geocode(addressString:) method, passing in the published element.

// MARK: - Helper Methods

private func setupBindings() {
    // Observe Query
    $query
        .sink { (addressString) in
            self.geocode(addressString: addressString)
        }
}

There are several issues we need to address. We don't want to strongly reference self, the view model, in the closure we pass to the sink(receiveValue:) method. We use a capture list to weakly reference self in the closure and use optional chaining to safely access the view model in the closure.

// MARK: - Helper Methods

private func setupBindings() {
    // Observe Query
    $query
        .sink { [weak self] (addressString) in
            self?.geocode(addressString: addressString)
        }
}

The compiler warns us that the result of the sink(receiveValue:) method is unused. The sink(receiveValue:) method creates a subscriber and returns it. This allows the view model to cancel the subscription at any time. This is important because the subscription should be cancelled when it is no longer needed, for example, when the view model is deallocated.

The return type of the sink(receiveValue:) method is AnyCancellable. You may expect AnyCancellable to be a protocol, but it isn't. It is defined as a class. Don't worry about the details for now. What you need to know is that AnyCancellable defines a cancel() method that cancels the subscription it references. What is nice about AnyCancellable is that it automatically invokes cancel() when it is deallocated.

It also defines two store(in:) methods. These methods make it trivial to add an AnyCancellable instance to a collection of AnyCancellable instances. This pattern is very similar to RxSwift's dispose bag pattern. The idea is almost identical.

We define a private, variable property, subscriptions, of type Set<AnyCancellable> and initialize it with an empty set.

private var subscriptions: Set<AnyCancellable> = []

In setupBindings(), we invoke the store(in:) method on the AnyCancellable instance that is returned by the sink(receiveValue:) method. Notice that we pass subscriptions as an in-out parameter by prefixing it with an ampersand.

// MARK: - Helper Methods

private func setupBindings() {
    // Observe Query
    $query
        .sink { [weak self] (addressString) in
            self?.geocode(addressString: addressString)
        }.store(in: &subscriptions)
}

Removing Duplicates

RxCocoa defines the distinctUntilChanged operator to only emit distinct contiguous elements. Combine defines a similar operator, removeDuplicates. We can apply this operator to the query publisher to ensure only distinct contiguous elements are received by the subscriber.

// MARK: - Helper Methods

private func setupBindings() {
    // Observe Query
    $query
        .removeDuplicates()
        .sink { [weak self] (addressString) in
            self?.geocode(addressString: addressString)
        }.store(in: &subscriptions)
}

Throttling

To limit the number of geocoding requests that are sent in a period of time, we apply the throttle operator. It accepts three arguments, (1) the interval at which to operate, (2) the scheduler on which to publish elements, and (3) a boolean value that indicates whether to publish the most recent element.

We instruct the throttle operator to publish the most recent element every half second and the element should be published on the main thread.

// MARK: - Helper Methods

private func setupBindings() {
    // Observe Query
    $query
        .removeDuplicates()
        .throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
        .sink { [weak self] (addressString) in
            self?.geocode(addressString: addressString)
        }.store(in: &subscriptions)
}

What's Next?

We reactified the AddLocationViewModel class with the help of the Combine framework. We can already see a few benefits. The query, querying, and locations properties no longer define didSet property observers and we removed the queryingDidChange and locationsDidChange properties. We exposed three publishers instead. In the next episode, we complete the integration of the Combine framework by refactoring the AddLocationViewController class.