iOS Bites — An Intro to Combine and the Reactive Pattern
You’ve heard of deep dives? iOS Bites is a series of short dives into new concepts and approaches in iOS development
At this year’s World Wide Developer Conference hosted by Apple, an event featuring the latest emerging technology for iOS developers, many of us were surprised by Apple’s announcement of their new Combine framework to the world. Previous to this, Apple had always advocated use of the MVC pattern in the development of applications for their platforms. Now they were seemingly throwing that away, replacing it with what is commonly referred to as the Reactive pattern. Was Apple replacing their tried and true way of writing apps with this new and unproven pattern?
In fact, nothing could be further from the truth. The Reactive pattern is a combination of the Observer pattern and the Iterator pattern, with a nice helping of functional programming to make things less verbose. Both the Observer pattern and iterators have been a part of the iOS core libraries since the first public release of the SDK. Functional programming first came to iOS development with the introduction of closures in Objective-C, coming into their own with the release of the Swift programming language.
So in that light, Reactive programming is simply the next step in the evolution of iOS development. In fact, Apple’s release trails community-sponsored libraries such as RxSwift, a four-year-old implementation of Reactive programming that’s part of the umbrella of ReactiveX libraries. But that still leaves us with the question — what problem is Reactive programming attempting to solve?
Using Reactive — A Simple Example
Let’s take the common use case of a mobile app consuming data from an API to display information on a screen. For this example, we will imagine a music platform which hosts an API that returns data about a user’s playlists. When the user opens the app, they are shown a list of their playlists. Upon clicking one, they can see all the details of that playlist such as songs and artists. All this information is slightly delayed by various factors. Some of those factors, like how long it takes to retrieve the information from a database, can be minimized. Others, like network congestion, will always be there and remain largely unpredictable.
Once we launch this action, we’ll have some intermediate steps we need to take. First, we’ll need to show some indicator that a non-deterministic call is taking place, aka we’re making a network call. When that network call comes back we’ll need to display the data retrieved and remove said indicator from the screen. Error states and cancelation on the part of the user will also need to be accounted for.
Typical Approach
Current practice dictates that we could try to pre-optimize for these situations by grabbing all these values as soon as the app starts up and displaying them when the user navigates to that page. But that leaves us in the position of potentially displaying outdated information. Also, we’ve now got to carry this information with us throughout the app, or store it locally creating a potential security risk. So the optimal situation would be to get it from the server when the user needs it.
The Reactive Solution
Enter the Combine framework. Combine sits on top of existing frameworks, to bring Reactive programming to iPhone development. The existing frameworks Combine makes use of are:
- Target/Action
- NotificationCenter
- URLSession
- Key-value observing
- Ad-hoc callbacks
By “combining” the use of these frameworks into one, developers can reduce the complexity of network and UI interactions. Additionally, large teams will benefit by using standards ordained by Apple, which is great for enterprise developers.
For our example, there are three key Combine concepts we need to be aware of:
- Publisher — In our example, the Publisher would be the networking layer which ingests the response from the API and lets any Subscribers know that the data has become available.
- Subscriber — Subscribers know that the data has become available. The Subscriber in this instance is the UI which displays the information about the user’s playlists.
- Operator — Sitting in between the Publisher and Subscriber is the Operator which filters and modifies the values that the Publisher pushes. A good use case for an Operator here would be to pre-fetch additional meta information for the first dozen playlists to help reduce perceived latency further.
Combine in Action
We can implement a simple version of this with the console using Playgrounds in Xcode 11+. Note that this was built using Xcode 11.2 Beta.
import Foundationlet name = Notification.Name.init("timerFired")
let publisher = NotificationCenter.default.publisher(for: name)Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
NotificationCenter.default.post(name: name, object: "First Event")
}publisher
.receive(on: RunLoop.main)
.sink(receiveValue: {
print("Received event \($0)")
}
)let name3 = Notification.Name.init("timerFired3")
let publisher3 = NotificationCenter.default.publisher(for: name3)Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { (timer) in
NotificationCenter.default.post(name: name3, object: "Second Event")
}publisher
.combineLatest(publisher3)
.receive(on: RunLoop.main)
.sink(receiveValue: {
print("Fired at \($0) and \($1)")
})
When we run this code we see the following output:
Received event name = timerFired, object = First Event
Received event name = timerFired, object = First Event
Received event name = timerFired, object = First Event
Fired at name = timerFired, object = First Event, name = timerFired3, object = Second Event
The first publisher is firing every second which we see the output of as “Received event” while the second publisher, “Fired at name”, is firing every three seconds. Using these publishers and subscribers in combination with an operator, combineLatest, we can see how the Combine framework can be used to quickly build an app on top of asynchronously firing events.
Optimistic UI
Combine is landing just when a new design pattern is starting to make its mark on the iOS landscape. Enter Optimistic UI which has been around for awhile but has mostly slid under the radar of both developers and users. When it comes to making apps feel fast and responsive, regardless of network latency or other bottlenecks, design plays an outsized role. For years now Apple has recommended in their HIG that for launch screens, apps skip the traditional loading screen with the app logo and instead show a blank version of an app’s first screen. With Optimistic UI, that pattern is applied to every screen where there’s a potential for lag. And powered by the Reactive pattern, we now know when the data is available for us so we can update the UI.
“A launch screen appears instantly when your app starts up and is quickly replaced with the app’s first screen, giving the impression that your app is fast and responsive.”
A prime example of using Optimistic UI is in the Messages app which comes bundled with iOS. When a user sends a message the message window is immediately updated to include the new message even though the user could be offline when “sending” it. A spinner is placed next to the message and depending on the result of the network call to post the message, either the spinner will disappear on success, or an error message such as “Not Delivered” will appear in red. All of this happens while the UI is fully responsive to any touches. This pattern applies to any received messages as well, with typing indicators showing three dots when the other party is typing.
Skeleton Screen
A popular implementation of Optimistic UI is the Skeleton Screen with the most recognized use case being the Facebook feed. When launching the app, instead of seeing a spinner and waiting for the information to finish downloading before showing content, the skeleton screen shows blocks that roughly correspond to how the data will be laid out, often times with a shimmer effect.
Both of these UI implementations rely on data being populated eventually, not immediately. They dovetail with needing to wait on asynchronous network operations to finish, while also giving the impression that an app is fast and responsive.
Back to our music platform example. With both a new programming and UI pattern at our disposal, we can update the experience to limit network usage while making the app appear fast and responsive. Upon opening the app and landing on the playlists page, a call is made to the API that returns just the playlist information. At the same time, a skeleton screen showing an outline of your playlist is displayed. That skeleton screen is then replaced with real data, such as songs and artists, once the API call returns.
Futures
The Reactive pattern is not the only attempt to make asynchronous computing easier to digest. Futures, sometimes referred to as Promises, also attempt to do this. However, the pattern tends to lead developers to start mixing synchronous and asynchronous functions, aka the colored function problem. In short, while synchronous (blue) functions can be called from asynchronous (red) functions, asynchronous functions can only be called from other asynchronous functions. As well, your program will most likely wind up adopting continuation-passing style, aka the pyramid of doom, which can make it difficult to trace program execution.
Conclusion
As mobile continues to gobble up market share and eyeballs, consumer expectations around fast, responsive apps will increase as well. Far from being a flavor of the month, the Reactive pattern is a well-established approach to meeting this need and Apple’s implementation of it via their Combine framework is an encouraging step for iOS developers.
Resources
- Introducing Combine
- Combine in Practice
- Receiving and Handling Events with Combine
- Problem Solving with Combine Swift
- A Swift Playground explaining the concepts of the Combine framework
Related Articles
DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.