We haven't simplified or improved the implementation of the setupNotificationHandling() method of the RootViewModel class by adding Combine to the mix. In fact, Combine introduced additional complexity and we now need to manage the subscriptions we create. It is true that Combine comes with a bit of overhead. The traditional Cocoa API seems to be the better choice. Right?
As I mentioned earlier, you don't need to stop using the asynchronous interfaces of the Cocoa frameworks. In this example, it makes sense to use the addObserver(forName:object:queue:using:) method to be notified when the application becomes active. Remember that Combine isn't an all or nothing solution. You can sprinkle bits of reactive programming in your projects or you can go all in. That is up to you. I suggest you start simple and get comfortable with the framework first.
Let's revisit the setupNotificationHandling() method of the RootViewModel class. I agree that the traditional Cocoa API seems to be the better choice in this use case. Before you draw any conclusions, though, I want to show you where Combine excels.
Working With Operators
The requestLocation() method is invoked every time the application becomes active. Is that the behavior we want? What happens if the user switches back and forth between Cloudy and another application. Every time the user switches to Cloudy a UIApplication.didBecomeActiveNotification notification is posted. That results in the requestLocation() method being invoked multiple times in a short period of time. The location of the user's device won't change significantly. Moreover, Cloudy only uses the location to fetch weather data for the location.
We should implement a solution that prevents the requestLocation() method from being invoked multiple times in a short period of time. Using the traditional Cocoa API of the Foundation framework, this isn't as simple as it sounds. We would need to keep track of the time the requestLocation() method was last invoked. That piece of data would need to be stored somewhere. Do you see how a small change becomes a complex problem rather quickly?
Transforming Streams of Data
I am going to spare you the solution. Let's instead look at how Combine can help us solve this problem. You learned that Combine works with streams of data or values over time. That is the unified interface the framework exposes. Those streams of data can be transformed and combined to create the result you need. We can transform and combine streams of data using operators. Operators are the third key player of the Combine framework.
What would a solution using Combine look like? We only need to add one line of code. We apply the throttle operator to limit the number of values the publisher emits in a period of time. The throttle operator is nothing more than a function that accepts three arguments, a time interval, a scheduler, and a boolean value.
We cover the throttle operator in more detail later in this series. What I want you to understand for now is that the throttle operator guarantees that only a single value is published in the time interval we pass to the throttle operator. The last argument, a boolean value, defines whether the resulting publisher emits the first value or the last value that was emitted in the time interval.
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: false)
.sink { notification in
print("did receive notification")
}.store(in: &subscriptions)
Before we run the application, let's add timestamps to the print statements to make it clear how the throttle operator transforms the stream of data published by the notification center. We create a DateFormatter instance and set its timeStyle property to medium. We use the date formatter to add a timestamp to each message we print to the console.
private func setupNotificationHandling() {
// Create Date Formatter
let dateFormatter = DateFormatter()
// Configure Date Formatter
dateFormatter.timeStyle = .medium
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
self?.requestLocation()
print("\(dateFormatter.string(from: Date())) did receive notification > NOT Reactive")
}
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: false)
.sink { notification in
print("\(dateFormatter.string(from: Date())) did receive notification > Reactive")
}.store(in: &subscriptions)
}
Let's run the application and explore what impact this small change has. Both print statements are executed on launch. This isn't surprising because UIKit posts a UIApplication.didBecomeActiveNotification notification on launch. Push the application to the background and bring it back to the foreground. Do this a few times to make it clear what the difference in implementation is. This is what the output looks like.
09:47:10 PM did receive notification > NOT throttled
09:47:10 PM did receive notification > throttled
09:47:15 PM did receive notification > NOT throttled
09:47:18 PM did receive notification > NOT throttled
09:47:25 PM did receive notification > throttled
09:47:26 PM did receive notification > NOT throttled
The first time I used the throttle operator, I couldn't believe how easy it was to implement throttling using operators. The timestamps show that the closure that is passed to the addObserver(forName:object:queue:using:) method is executed every time the application enters the foreground. Thanks to the throttle operator, the closure that is passed to the sink(_:) method is executed once at most every ten seconds. That is the power and convenience of reactive programming.
What's Next?
You have written your first lines of reactive code and I hope you are intrigued by the possibilities reactive programming has to offer. Reactive programming and the Combine framework have a lot more to offer, though. In the next episodes, we take a close look at publishers, subscribers, and operators, the key players of the Combine framework.