It is time to show you what Combine feels like and what it can do for you. What do you gain by using the Combine framework in your projects? Let's start with some good news. The Combine framework has a relatively small vocabulary. You don't need to spend days or weeks familiarizing yourself with a plethora of types, protocols, and terminology.

Remember the definition from earlier in this series.

Reactive programming is working with publishers that asynchronously publish events to which subscribers can subscribe.

This episode focuses on publishers and subscribers. We use Cloudy to get your feet wet with Combine. Cloudy is a simple weather application that fetches weather data and displays it to the user. You may remember Cloudy from Mastering MVVM With Swift. Don't worry if you are unfamiliar with the Model-View-ViewModel pattern. This series focuses on reactive programming with Combine.

Publishers and Subscribers

Open RootViewModel.swift and navigate to the setupNotificationHandling() method. This helper method is invoked during initialization. Its implementation should look familiar if you have worked with notifications.

private func setupNotificationHandling() {
    NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
        self?.requestLocation()
    }
}

The view model registers a closure with the default notification center for UIApplication.didBecomeActiveNotification notifications. Earlier in this series, you learned that notifications are an example of the observer pattern. The notification center broadcasts a UIApplication.didBecomeActiveNotification notification every time the application becomes active. By invoking the addObserver(forName:object:queue:using:) method on the default notification center, the closure that is passed as the last argument is executed every time the application becomes active. In the closure, the view model executes its requestLocation() method.

Let's translate this to Combine vocabulary. The notification center broadcasts notifications. To keep it simple, we could say that the notification center is the publisher in this example. It emits an event, a notification, every time the application becomes active. The subscriber in this example isn't the view model. That is a common misconception. The subscriber is the closure that is passed to the addObserver(forName:object:queue:using:) method.

We translated the example to Combine vocabulary, but we are still not using the Combine framework. Let's start by adding an import statement for the Combine framework at the top of RootViewModel.swift.

import UIKit
import Combine
import CoreLocation

Earlier I mentioned that the Combine framework is integrated into several system frameworks, including the Foundation framework. We can ask the notification center for a publisher by invoking the publisher(for:) method, passing in the name of the notification we are interested in.

NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)

The publisher(for:) method returns a publisher. To put it to use, we need to subscribe to the publisher. The Combine framework gives us two options. The option that makes the most sense in this example is the sink(_:) method. It attaches a subscriber to the publisher that is returned from the publisher(for:) method.

The sink(_:) method accepts a closure. Like the addObserver(forName:object:queue:using:) method, the closure is executed every time the notification center broadcasts a UIApplication.didBecomeActiveNotification notification.

NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
    .sink { notification in

    }

The closure that is passed to the sink(_:) method accepts the value the publisher emits. In this example, that value is a notification.

Let's test the implementation. Add a print statement to the closure we pass to the addObserver(forName:object:queue:using:) method. We do the same for the closure we pass to the sink(_:) method. We use the print statements to verify the closures are executed every time the application becomes active.

private func setupNotificationHandling() {
    NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
        self?.requestLocation()
        print("did receive notification > NOT Reactive")
    }

    NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
        .sink { notification in
            print("did receive notification > Reactive")
        }
}

Run the application and inspect the output in the console. You may be surprised by the output. Only the print statement we added to the closure that was passed to the addObserver(forName:object:queue:using:) method was executed. Why is that?

did receive notification > NOT Reactive

Creating a Subscription

You probably noticed that we ignored a compiler warning. The compiler warns us that we make no use of the return value of the sink(_:) method. What is that about?

The compiler warned us something isn't quite right.

Option click the sink(_:) method to inspect the return type of the sink(_:) method.

The sink method returns an AnyCancellable instance.

The sink(_:) method returns an object of type AnyCancellable. We take a closer look at AnyCancellable later in this series. What I want you to understand is that Combine creates a subscription when a subscriber is attached to a publisher. That makes sense. Right?

When you no longer need the subscription, you need to cancel it. That is what the AnyCancellable instance is for. It provides the option to cancel the subscription that is created when a subscriber is attached to a publisher. To cancel the subscription, you call cancel() on the AnyCancellable instance.

You rarely need to do this yourself, though. An AnyCancellable instance automatically calls cancel() when it is about to be deallocated. That is convenient. Right?

There is an important gotcha, though. The compiler warns us that we are not using the return value of the sink(_:) method. In other words, we are not holding on to the AnyCancellable instance. What does that mean?

If we don't keep a reference to the AnyCancellable instance, it is deallocated when execution leaves the current scope. In other words, the AnyCancellable instance is deallocated when execution leaves the setupNotificationHandling() method. Remember what I said a few moments ago. An AnyCancellable instance calls cancel() when it is about to be deallocated. The subscription is cancelled if we don't keep a reference to the AnyCancellable instance.

The solution is straightforward and we can keep it simple for now. We declare a private, variable property of type AnyCancellable? and use it to keep a reference to the AnyCancellable instance. This is convenient because the AnyCancellable instance is deallocated when the view model is deallocated. When that happens the AnyCancellable instance cancels the subscription, which is exactly what we want.

private var subscription: AnyCancellable?

private func setupNotificationHandling() {
    NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
        self?.requestLocation()
        print("did receive notification > NOT Reactive")
    }

    subscription = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
        .sink { notification in
            print("did receive notification > Reactive")
        }
}

Let's run the application to make sure that everything works as expected. The output in the console confirms that the solution we implemented works. The AnyCancellable instance is stored in the subscription property, keeping it alive for as long as the view model is alive.

did receive notification > NOT Reactive
did receive notification > Reactive

The output in the console confirms that we created a publisher and successfully subscribed to it. The closure we passed to the sink(_:) method is executed every time the application becomes active. This is the Combine framework in a nutshell.

What's Next?

This episode touched on the essence of reactive programming and the Combine framework. Reactive programming is about creating and subscribing to streams of data or values over time. While there is more to reactive programming and the Combine framework, understanding this fundamental concept is a significant accomplishment.