The Cocoa frameworks use a range of asynchronous interfaces, including the target-action pattern, key-value observing, notifications, and callbacks. We can leverage the Combine framework to create a single, unified interface for asynchronous programming. This opens up a number of compelling advantages.

In this and the next episodes, we revisit Cloudy. We identify the asynchronous interfaces the project uses and explore how the Combine framework can optimize and simplify the codebase. I promise you that the result is elegant, declarative, and maintainable.

Published Property Wrapper

The Combine framework has a number of APIs we can use to reactify the code we write. I would like to start with what is arguably the easiest API to get your feet wet with Combine, the Published property wrapper. Property wrappers were introduced in Swift 5.1 to eliminate boilerplate code, facilitate code reuse, and enable more expressive APIs. If you are new to property wrappers, then I strongly recommend you take a look at the short series I created about property wrappers.

If you plan to integrate the Combine framework into an existing project, I recommend you start with the low-hanging fruit. Open the Find Navigator on the right and search the project for didSet property observers. The use of didSet property observers is often a code smell in a project that embraces reactive programming.

Xcode reveals that we use a didSet property observer in the RootViewModel class. Open RootViewModel.swift and navigate to the currentLocation property. Every time currentLocation is set, the currentLocationDidChange handler is invoked.

// MARK: - Properties

var currentLocationDidChange: (() -> Void)?

// MARK: -

private(set) var currentLocation: CLLocation? {
    didSet {
        currentLocationDidChange?()
    }
}

There is nothing wrong with this implementation, but it isn't reactive. We can improve the implementation by using Combine's Published property wrapper. We apply the Published property wrapper to currentLocation by annotating the property with the Published attribute.

@Published private(set) var currentLocation: CLLocation? {
    didSet {
        currentLocationDidChange?()
    }
}

What does this mean? The Published property wrapper creates a publisher of the property's type, which you can access through the property wrapper's projected value. I explain this in detail in Working With Property Wrappers: Initial Values and Projected Values. What I want you to understand is that the Published property wrapper creates a publisher that emits CLLocation objects. Every time the value of currentLocation changes, the publisher emits the value of currentLocation and that is what we need.

The root view controller no longer needs to use the currentLocationDidChange handler of its view model to be notified of location changes. It can subscribe to the publisher of the currentLocation property instead. Before we update the RootViewController class, we remove the didSet property observer and the currentLocationDidChange property.

// MARK: - Properties

@Published private(set) var currentLocation: CLLocation?

Subscribing to the Publisher

Open RootViewController.swift and add an import statement for the Combine framework at the top.

import UIKit
import Combine

final class RootViewController: UIViewController {

    ...

}

Navigate to the viewDidLoad() method and remove the lines that set the view model's currentLocationDidChange property. We invoke a helper method with name setupBindings().

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup Bindings
    setupBindings()
}

Before we implement the setupBindings() method, we declare a private, variable property, subscriptions, of type Set<AnyCancellable>. The default value of subscriptions is an empty set. The subscriptions property stores the subscriptions we create in the RootViewController class. We covered this earlier in this series.

private var subscriptions: Set<AnyCancellable> = []

The implementation of setupBindings() isn't difficult if you watched the previous episodes. We ask the view model for the publisher of the currentLocation property. The Published property wrapper exposes the publisher through its projected value hence the $. The view controller creates a subscription by invoking the sink(receiveValue:) method on the publisher. We use a capture list to weakly reference self, the view controller, in the closure. In the closure that is passed to the sink(receiveValue:) method, the view controller invokes the fetchWeatherData() method. The subscription returned by the sink(receiveValue:) method is added to the set of subscriptions with the help of the store(in:) method.

// MARK: - Helper Methods

private func setupBindings() {
    viewModel?.$currentLocation
        .sink(receiveValue: { [weak self] _ in
            self?.fetchWeatherData()
        }).store(in: &subscriptions)
}

Breaking Cloudy

Build and run the application to make sure the behavior of the application hasn't changed. Cloudy presents the user with a warning. It notifies the user that it isn't able to fetch weather data. Something isn't quite right.

There are two issues we need to address. Let's start with the first issue. The initial value of the currentLocation property is equal to nil and that is the first value the currentLocation publisher emits. The view model shouldn't attempt to fetch weather data if currentLocation is equal to nil. This is easy to resolve using an operator.

The second issue is less obvious and it trips up many developers that are new to Combine and the Published property wrapper. When the value of the currentLocation property changes, the currentLocation publisher emits the new value. The currentLocation property is updated with the new value after the currentLocation publisher emits the new value. This sequence of events is causing the issue we are seeing.

Let me explain this in more detail because it is important that you understand why the changes we made break the application. The view model updates the value of the currentLocation property. As a result, the currentLocation publisher emits the new value. The view controller invokes the fetchWeatherData() method every time the value of the currentLocation property changes. The fetchWeatherData() method invokes the fetchWeatherData(_:) method on its view model. The problem is that the view model uses the value of its currentLocation property to fetch weather data. As I mentioned a few moments ago, the value of the currentLocation property is updated with the new value after the currentLocation publisher emits the new value. The value of currentLocation is equal to nil when the view controller invokes the fetchWeatherData(_:) method on its view model.

I agree this is confusing and it is unclear why the Published property wrapper works this way. It is possible that the Published property wrapper uses a willSet property observer under the hood. That might explain the behavior of the Published property wrapper.

What's Next?

The Published property wrapper offers a great entry point into the Combine framework, but, as you can see, there are a few caveats you need to watch out for. In the next episode, we take a closer look at both issues and resolve them in true Combine fashion.

When I come across a didSet property observer in an existing project, I wonder if the implementation can be improved using reactive programming. This episode shows how a small change can result in an elegant and simplified result.