The root view controller displays the weather data the publisher emits. Every time the publisher emits weather data, the child view controllers of the root view controller receive the weather data and display it to the user. This works fine, but we can make it more reactive.

Let's take the day view controller as an example. Every time the publisher of the root view model emits weather data, the root view controller creates a DayViewModel object and passes it to the day view controller. This is another fine example of imperative programming that we can refactor using the Combine framework. Let me show you what I have in mind.

Reactifying the Day View Controller

There is no need to create a DayViewModel object every time the weather data changes. We can inject the weather data and weather data error publishers of the root view model into the DayViewModel object. If the day view controller subscribes to the publishers of its view model, its user interface automatically updates when the publishers emit data. That is a more reactive solution.

Updating the Day View Model

We start with the DayViewModel struct. Open DayViewModel.swift and remove the weatherData property. We no longer need it. Declare a constant property, weatherDataPublisher, of type AnyPublisher<WeatherData?, Never>. The Output type of the publisher is WeatherData? and the Failure type is Never.

AnyPublisher is a struct that conforms to the Publisher protocol. Don't worry about it for now. We discuss AnyPublisher in more detail in a moment.

import UIKit
import Combine

struct DayViewModel {

    // MARK: - Properties

    let weatherDataPublisher: AnyPublisher<WeatherData?, Never>

    ...

}

We declare another constant property, weatherDataErrorPublisher, of type AnyPublisher<WeatherDataError?, Never>. The Output type of the publisher is WeatherDataError? and the Failure type is Never.

import UIKit
import Combine

struct DayViewModel {

    // MARK: - Properties

    let weatherDataPublisher: AnyPublisher<WeatherData?, Never>
    let weatherDataErrorPublisher: AnyPublisher<WeatherDataError?, Never>

    ...

}

Type Erasure

Open RootViewController.swift and navigate to the setupBindings() method. We no longer create a DayViewModel object and pass it to the day view controller.

private func setupBindings() {
    viewModel?.$weatherData
        .compactMap { $0 }
        .sink(receiveValue: { [weak self] weatherData in
            // Configure Week View Controller
            self?.weekViewController.viewModel = WeekViewModel(weatherData: weatherData.dailyData)
        }).store(in: &subscriptions)

    ...
}

The root view controller creates a DayViewModel object in the prepare(for:sender:) method and passes it to the day view controller by setting its viewModel property. Remember that the memberwise initializer of the DayViewModel struct accepts a weather data publisher and a weather data error publisher. We can ask the root view model for these publishers. Let's give that a try.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let viewModel = viewModel, let identifier = segue.identifier else {
        return
    }

    switch identifier {
    case Segue.dayView:
        guard let destination = segue.destination as? DayViewController else {
            fatalError("Unexpected Destination View Controller")
        }

        // Configure Destination
        destination.delegate = self

        // Update Day View Controller
        self.dayViewController = destination
        self.dayViewController.viewModel = DayViewModel(weatherDataPublisher: viewModel.$weatherData,
                                                        weatherDataErrorPublisher: viewModel.$weatherDataError)
        ...
    }
}

The compiler doesn't seem to agree with this solution. It throws a vague error. What is the problem?

The compiler doesn't seem to agree with this solution.

The problem is that the type of the weather data publisher of the root view model doesn't match the type of the weatherDataPublisher property of the DayViewModel struct. The weatherDataPublisher property is of type AnyPublisher<WeatherData?, Never>. The weather data publisher of the root view model is of type Published<WeatherData?>.Publisher. Even though both properties are publishers that emit WeatherData objects, the types don't match and that is why the compiler throws an error. This is a common problem when working with the Combine framework.

There actually are two problems we need to address, not one. You already know about the first problem, the types don't match. The second problem is more subtle. The root view model exposes details about its implementation that it shouldn't expose. The root view controller asks its view model for the publisher of the weatherData property. The Published property wrapper is convenient, but should the root view controller know about this implementation detail? It is only interested in a publisher that emits weather data.

We can address both problems with only a few lines of code. Open RootViewModel.swift and declare a computed property, weatherDataPublisher, of type AnyPublisher<WeatherData?, Never>. The computed property returns the publisher of the weatherData property.

import UIKit
import Combine
import CoreLocation

final class RootViewModel: NSObject {

    // MARK: - Properties

    @Published private(set) var currentLocation: CLLocation?

    // MARK: -

    var weatherDataPublisher: AnyPublisher<WeatherData?, Never> {
        $weatherData
    }

    @Published private(set) var weatherData: WeatherData?
    @Published private(set) var weatherDataError: WeatherDataError?

    ...

}

The compiler throws another error, but this time the error is more helpful. It confirms what I explained earlier. The type of the computed property and the type of the weather data publisher don't match.

The compiler throws another error, but this time the error is more helpful.

This brings us to the AnyPublisher type. What is AnyPublisher? Apple's description is short and to the point. It is a publisher that performs type erasure by wrapping another publisher.

A publisher that performs type erasure by wrapping another publisher.

To understand this definition, you need to become familiar with type erasure. As the name suggests, type erasure is the concept of erasing or hiding a type. We could change the type of the weatherDataPublisher property, but that isn't what we want. We don't want other objects to know about the implementation details of the RootViewModel class. That is where the AnyPublisher type comes in. It enables us to define a publisher that wraps another publisher and, as a result, hides the type of the wrapped publisher. Let me show you how this works.

We don't need to create an AnyPublisher instance. The Combine framework defines a convenient helper method, eraseToAnyPublisher, that converts a publisher to an AnyPublisher instance. We simply call eraseToAnyPublisher on the publisher of the weatherData property.

var weatherDataPublisher: AnyPublisher<WeatherData?, Never> {
    $weatherData
        .eraseToAnyPublisher()
}

We can implement the same solution for the publisher of the weatherDataError property. We declare a computed property, weatherDataErrorPublisher, of type AnyPublisher<WeatherDataError, Never>. In the body of the computed property, we wrap the publisher of the weatherDataError property with a type eraser using the eraseToAnyPublisher() method.

var weatherDataErrorPublisher: AnyPublisher<WeatherDataError?, Never> {
    $weatherDataError
        .eraseToAnyPublisher()
}

Revisit RootViewController.swift and update the implementation of the prepare(for:sender:) method. To create the DayViewModel object, the root view controller uses the weatherDataPublisher and weatherDataErrorPublisher properties of its view model.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let viewModel = viewModel, let identifier = segue.identifier else {
        return
    }

    switch identifier {
    case Segue.dayView:
        guard let destination = segue.destination as? DayViewController else {
            fatalError("Unexpected Destination View Controller")
        }

        // Configure Destination
        destination.delegate = self

        // Update Day View Controller
        self.dayViewController = destination
        self.dayViewController.viewModel = DayViewModel(weatherDataPublisher: viewModel.weatherDataPublisher,
                                                        weatherDataErrorPublisher: viewModel.weatherDataErrorPublisher)
        ...
    }
}

There is one change I want to make before we continue. The Output types of the publishers of the DayViewModel struct have an optional type. That doesn't make much sense. The optionality is the result of the weatherData and weatherDataError properties of the RootViewModel class. We can improve the implementation by applying the compactMap operator in the RootViewModel class. With this simple change, the publishers of the RootViewModel class no longer have an optional Output type.

var weatherDataPublisher: AnyPublisher<WeatherData, Never> {
    $weatherData
        .compactMap { $0 }
        .eraseToAnyPublisher()
}

var weatherDataErrorPublisher: AnyPublisher<WeatherDataError, Never> {
    $weatherDataError
        .compactMap { $0 }
        .eraseToAnyPublisher()
}

This also means we need to update the properties of the DayViewModel struct.

import UIKit
import Combine

struct DayViewModel {

    // MARK: - Properties

    let weatherDataPublisher: AnyPublisher<WeatherData, Never>
    let weatherDataErrorPublisher: AnyPublisher<WeatherDataError, Never>

    ...

}

AnyPublisher and AnyCancellable

You may remember that we briefly touched on the concept of type erasure earlier in this series when we discussed the Cancellable protocol and the AnyCancellable class. Open RootViewModel.swift and navigate to the setupNotificationHandling() method. The RootViewModel class executes the sink(receiveValue:) method to attach a subscriber. The type that does the heavy lifting is Subscribers.Sink, a class conforming to the Subscriber protocol and the Cancellable protocol.

private func setupNotificationHandling() {
    NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
        .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: false)
        .sink { [weak self] _ in
            self?.requestLocation()
        }.store(in: &subscriptions)
}

But the sink(receiveValue:) method doesn't return a Cancellable object. It returns an instance of the AnyCancellable class. Like the AnyPublisher class, the AnyCancellable class wraps another object. The AnyCancellable class erases or hides the type of the object it wraps. The only requirement is that the wrapped object conforms to the Cancellable protocol. The result is an API that hides any implementation details of the Combine framework consumers of the API don't need to know about.

What's Next?

This episode introduces an important concept, type erasure. The Combine framework relies on type erasure so it is essential that you understand what it is and how the framework takes advantage of this concept.