In the previous episode, we put the foundation in place to reactify the DayViewController class. In this episode, you learn how to use publishers to drive the user interface of an application.

Creating Publishers to Drive the User Interface

Open DayViewModel.swift. The DayViewModel struct defines a collection of computed properties the day view controller uses to populate its user interface. This is another example of imperative programming. Let's use the Combine framework to reactify the DayViewModel struct. We start with the date computed property. We rename the computed property to datePublisher and change its type to AnyPublisher<String, Never>. The publisher's Output type is String and its Failure type is Never.

var datePublisher: AnyPublisher<String, Never> {
    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return dateFormatter.string(from: weatherData.time)
}

We need to make a few changes. First, we remove the dateFormatter property of the DayViewModel struct and create a DateFormatter instance in the body of the computed property.

var datePublisher: AnyPublisher<String, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return dateFormatter.string(from: weatherData.time)
}

The next step is surprisingly simple. The day view model returns the weather data publisher. It applies the map operator to transform the values the publisher emits. We use the date formatter to convert the value of the time property, a Date object, to a string.

var datePublisher: AnyPublisher<String, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return weatherDataPublisher
        .map { dateFormatter.string(from: $0.time) }
}

The compiler throws an error, but that isn't surprising. We ran into the same error in the previous episode and we know how to fix it. We wrap the publisher with a type eraser using the eraseToAnyPublisher() method. We covered this in the previous episode.

var datePublisher: AnyPublisher<String, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return weatherDataPublisher
        .map { dateFormatter.string(from: $0.time) }
        .eraseToAnyPublisher()
}

You may be wondering why we don't initialize the date formatter in the closure that is passed to the map operator. That is an option, but the result is less performant. Remember that the closure is executed every time the publisher emits a value. If we were to initialize the date formatter in the closure that is passed to the map operator, we would create a date formatter every time the publisher emits a value. That isn't the best use of resources. The day view model creates the date formatter once in the body of the datePublisher computed property. The closure that is passed to the map operator captures the date formatter, using the same date formatter every time it is executed.

Driving the User Interface

Before we update the other computed properties of the DayViewModel struct, I want to show you how to use the publisher we created to drive the user interface of the day view controller. Open DayViewController.swift and add an import statement for the Combine framework at the top.

import UIKit
import Combine

Declare a private, variable property, subscriptions, of type Set<AnyCancellable>. The initial value of the subscriptions property is an empty set. This should look familiar by now.

private var subscriptions: Set<AnyCancellable> = []

Override the viewDidLoad() method and invoke setupBindings(), a helper method.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup Bindings
    setupBindings()
}

In setupBindings(), the day view controller attaches a subscriber to datePublisher by invoking the sink(receiveValue:) method. We use a capture list to weakly reference the view controller in the closure that is passed to the sink(receiveValue:) method. In the closure, we update the text property of the date label. The AnyCancellable instance the sink(receiveValue:) method returns is added to subscriptions. This should look familiar.

// MARK: - Helper Methods

private func setupBindings() {
    viewModel?.datePublisher
        .sink(receiveValue: { [weak self] date in
            self?.dateLabel.text = date
        }).store(in: &subscriptions)
}

This solution works and it doesn't look too bad, but the Combine framework has a more elegant API to achieve the same result. We can also attach a subscriber to datePublisher by invoking the assign(to:on:) method. It accepts a key path as its first argument and an object as its second argument. How does this work?

The values emitted by datePublisher are assigned to a property of the object, the second argument, using the key path, the first argument. We don't need to pass in a closure or use a capture list to avoid a strong reference cycle. The result is easy to read and understand.

// MARK: - Helper Methods

private func setupBindings() {
    viewModel?.datePublisher
        .assign(to: \.text, on: dateLabel)
        .store(in: &subscriptions)
}

The compiler doesn't agree, though. It throws an error that doesn't make much sense. Don't worry. The solution is quite simple. The values emitted by datePublisher are of type String. The text property of dateLabel is of type String?. The types don't match and that is why the compiler throws an error. The Combine framework isn't very forgiving so we need to make a small change.

Revisit DayViewModel.swift and navigate to the datePublisher computed property. We need to change the Output type of the publisher from String to String?. Even though we know the publisher won't emit nil, this change is necessary to resolve the error.

var datePublisher: AnyPublisher<String?, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return weatherDataPublisher
        .map { dateFormatter.string(from: $0.time) }
        .eraseToAnyPublisher()
}

Updating the other computed properties isn't too difficult. Pause the video and give it a try. This is what the result should look like.

// MARK: - Public API

var datePublisher: AnyPublisher<String?, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEE, MMMM d"

    return weatherDataPublisher
        .map { dateFormatter.string(from: $0.time) }
        .eraseToAnyPublisher()
}

var timePublisher: AnyPublisher<String?, Never> {
    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = UserDefaults.timeNotation.dateFormat

    return weatherDataPublisher
        .map { dateFormatter.string(from: $0.time) }
        .eraseToAnyPublisher()
}

var summaryPublisher: AnyPublisher<String?, Never> {
    weatherDataPublisher
        .map { $0.summary }
        .eraseToAnyPublisher()
}

var temperaturePublisher: AnyPublisher<String?, Never> {
    weatherDataPublisher
        .map {
            switch UserDefaults.temperatureNotation {
            case .fahrenheit:
                return String(format: "%.1f °F", $0.temperature)
            case .celsius:
                return String(format: "%.1f °C", $0.temperature.toCelcius)
            }
        }
        .eraseToAnyPublisher()
}

var windSpeedPublisher: AnyPublisher<String?, Never> {
    weatherDataPublisher
        .map {
            switch UserDefaults.unitsNotation {
            case .imperial:
                return String(format: "%.f MPH", $0.windSpeed)
            case .metric:
                return String(format: "%.f KPH", $0.windSpeed.toKPH)
            }
        }
        .eraseToAnyPublisher()
}

var imagePublisher: AnyPublisher<UIImage?, Never> {
    weatherDataPublisher
        .map { UIImage.imageForIcon(with: $0.icon) }
        .eraseToAnyPublisher()
}

With the DayViewModel struct updated, we can continue refactoring the DayViewController class. Open DayViewController.swift and navigate to the setupBindings() method. We use the assign(to:on:) method to drive the user interface elements of the day view controller.

// MARK: - Helper Methods

private func setupBindings() {
    viewModel?.datePublisher
        .assign(to: \.text, on: dateLabel)
        .store(in: &subscriptions)

    viewModel?.timePublisher
        .assign(to: \.text, on: timeLabel)
        .store(in: &subscriptions)

    viewModel?.windSpeedPublisher
        .assign(to: \.text, on: windSpeedLabel)
        .store(in: &subscriptions)

    viewModel?.temperaturePublisher
        .assign(to: \.text, on: temperatureLabel)
        .store(in: &subscriptions)

    viewModel?.summaryPublisher
        .assign(to: \.text, on: descriptionLabel)
        .store(in: &subscriptions)

    viewModel?.imagePublisher
        .assign(to: \.image, on: iconImageView)
        .store(in: &subscriptions)
}

Before we test the changes, we need to clean up the DayViewController class. We can remove the didSet property observer of the viewModel property. Because the user interface is automatically updated when the weather data publisher emits weather data, there is no need to invoke the updateView() method. A property observer is often a code smell in a project that embraces reactive programming.

var viewModel: DayViewModel?

We can also remove the updateWeatherDataContainerView(with:) method because we no longer need to manually update the user interface of the day view controller. In the updateView() method, we remove the reference to the updateWeatherDataContainerView(with:) method and set the isHidden property of weatherDataContainerView to false. This is a temporary solution, though. We improve the implementation in the next episode.

// MARK: - View Methods

private func updateView() {
    activityIndicatorView.stopAnimating()

    weatherDataContainerView.isHidden = false

    if let viewModel = viewModel {

    } else {
        messageLabel.isHidden = false
        messageLabel.text = "Cloudy was unable to fetch weather data."
    }
}

Invoke the updateView() method in viewDidLoad() and run the application in a simulator or on a physical device.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup Bindings
    setupBindings()

    // Update View
    updateView()
}

Even though we still need to make some tweaks here and there, we successfully reactified the day view controller. The user interface of the day view controller is driven by the publishers of the day view model.

Driving a User Interface with Combine

What's Next?

We achieved a significant milestone in this episode. With the help of the assign(to:on:) method, the day view model drives the user interface of the day view controller. The day view controller is automatically updated every time the root view model successfully fetches weather data.

It is important to realize that the day view model no longer manages state. It doesn't hold on to a WeatherData object. It uses streams of values that change over time to drive the user interface of the day view controller. That is the essence of reactive programming.