This book aims to introduce you to the Combine framework and to writing declarative, reactive apps with Swift for Apple platforms.
In Apple’s own words: “The Combine framework provides a declarative approach for how your app processes events. Rather than potentially implementing multiple delegate callbacks or completion handler closures, you can create a single processing chain for a given event source. Each part of the chain is a Combine operator that performs a distinct action on the elements received from the previous step.”
Although very accurate and to the point, this delightful definition might sound a little too abstract at first. That’s why, before delving into coding exercises and working on projects in the following chapters, you’ll take a little time to learn a bit about the problems Combine solves and the tools it uses to do so.
Once you’ve built up the relevant vocabulary and some understanding of the framework in general, you’ll move on to covering the basics while coding.
Gradually, as you progress in the book, you’ll learn about more advanced topics and eventually work through several projects.
When you’ve covered everything else you will, in the last chapter, work on a complete app built with Combine.
In a simple, single-threaded language, a program executes sequentially line-by-line. For example, in pseudocode:
begin var name = "Tom" print(name) name += " Harding" print(name) end
Synchronous code is easy to understand and makes it especially easy to argue about the state of your data. With a single thread of execution, you can always be sure what the current state of your data is. In the example above, you know that the first
Now, imagine you wrote the program in a multi-threaded language that is running an asynchronous event-driven UI framework, like an iOS app running on Swift and UIKit.
Consider what could potentially happen:
--- Thread 1 --- begin var name = "Tom" print(name) --- Thread 2 --- name = "Billy Bob" --- Thread 1 --- name += " Harding" print(name) end
Here, the code sets
name‘s value to
"Tom" and then adds
"Harding" to it, just like before. But because another thread could execute at the same time, it’s possible that some other part of your program could run between the two mutations of
name and set it to another value like
When the code is running concurrently on different cores, it’s difficult to say which part of the code is going to modify the shared state first.
The code running on “Thread 2” in the example above might be:
- executing at exactly the same time on a different CPU core as your original code.
- executing just before
name += " Harding", so instead of the original value
"Tom", it gets
What exactly happens when you run this code depends on the system load, and you might see different results each time you run the program.
Managing mutable state in your app becomes a loaded task once you run asynchronous concurrent code.
Foundation and UIKit/AppKit
Apple has been continually improving asynchronous programming for their platforms over the years. They’ve created several mechanisms you can use, on different system levels, to create and execute asynchronous code.
You can use APIs as low-level as managing your own threads with
NSThread all the way up to using Swift’s modern concurrency with the
You’ve probably used at last some of the following in your apps:
NotificationCenter: Executes a piece of code any time an event of interest happens, such as when the user changes the orientation of the device or when the software keyboard shows or hides on the screen.
- The delegate pattern: Lets you define an object that acts on behalf of, or in coordination with, another object. For example, in your app delegate, you define what should happen when a new remote notification arrives, but you have no idea when this piece of code will run or how many times it will execute.
- Grand Central Dispatch and Operations: Helps you abstract the execution of pieces of work. You can use them to schedule code to run sequentially in a serial queue or to run a multitude of tasks concurrently in different queues with different priorities.
- Closures: Create detached pieces of code that you can pass around in your code, so other objects can decide whether to execute it, how many times, and in what context.
Since most typical code performs some work asynchronously, and all UI events are inherently asynchronous, it’s impossible to make assumptions about which order the entirety of your app code will execute.
And yet, writing good asynchronous programs is possible. It’s just more complex than… well, we’d like it to be.
Certainly, one of the causes for these issues is the fact that a solid, real-life app most likely uses all the different kinds of asynchronous APIs, each with its own interface, like so:
Combine introduces a common, high-level language to the Swift ecosystem to design and write asynchronous code.
Apple has integrated Combine into its other frameworks too, so
NotificationCenter and core frameworks like Core Data already speak its language. Luckily, Combine is also very easy to integrate into your own code.
Finally, last but definitely not least, Apple designed their amazing UI framework, SwiftUI, to integrate easily with Combine as well.
To give you an idea of how committed Apple is to reactive programming with Combine, here’s a simple diagram showing where Combine sits in the system hierarchy:
Various system frameworks, from Foundation all the way up to SwiftUI, depend on Combine and offer Combine integration as an alternative to their more “traditional” APIs.
Swift’s Modern Concurrency
Swift 5.5 introduces a range of APIs for developing asynchronous and concurrent code which, thanks to a new threading-pool model, allows your code to safely and quickly suspend and resume asynchronous work at will.
The modern concurrency APIs make many of the classic async problems fairly easy to solve - for example waiting on a network response, running multiple tasks in parallel, and more.
These APIs solve some of the same problems as Combine does, but Combine’s strength lays in its rich set of operators. The operators that Combine offers for processing events over time make a lot of complex, common scenarios easy to address.
Reactive operators directly address a variety of common problems in networking, data processing, and handling UI events so for more complex applications there’s a lot of benefit in developing with Combine.
And, speaking of Combine’s strengths, let’s have a quick look at reactive programming’s excellent track so far.
Foundation of Combine
Declarative, reactive programming isn’t a new concept. It’s been around for quite a while, but it’s made a fairly noticeable comeback in the last decade.
The first “modern-day” reactive solution came in a big way in 2009 when a team at Microsoft launched a library called Reactive Extensions for .NET (Rx.NET).
Microsoft made that Rx.NET implementation open source in 2012, and since then, many different languages have started to use its concepts. Currently, there are many ports of the Rx standard like RxJS, RxKotlin, RxScala, RxPHP and more.
For Apple’s platforms, there have been several third-party reactive frameworks like RxSwift, which implements the Rx standard; ReactiveSwift, directly inspired by Rx; Interstellar, which is a custom implementation and others.
Combine implements a standard that is different but similar to Rx, called Reactive Streams. Reactive Streams has a few key differences from Rx, but they both agree on most of the core concepts.
If you haven’t previously used one or another of the frameworks mentioned above — don’t worry. So far, reactive programming has been a rather niche concept for Apple’s platforms, and especially with Swift.
In iOS 13/macOS Catalina, however, Apple brought reactive programming support to its ecosystem via the built-in system framework, Combine.
With that said, start by learning some of Combine’s basics to see how it can help you write safe and solid asynchronous code.
In broad strokes, the three key moving pieces in Combine are publishers, operators and subscribers. There are, of course, more players in the team, but without those three you can’t achieve much.
You’ll learn in detail about publishers and subscribers in Chapter 2, “Publishers & Subscribers,” and the complete second section of the book guides you through acquainting you with as many operators as humanly possible.
In this introductory chapter, however, you’re going to get a simple crash course to give you a general idea of the purpose those types have in the code and what their responsibilities are.
Publishers are types that can emit values over time to one or more interested parties, such as subscribers. Regardless of the internal logic of the publisher, which can be pretty much anything including math calculations, networking or handling user events, every publisher can emit multiple events of these three types:
- An output value of the publisher’s generic
- A successful completion.
- A completion with an error of the publisher’s
A publisher can emit zero or more output values, and if it ever completes, either successfully or due to a failure, it will not emit any other events.
Here’s how a publisher emitting
Int values could look like visualized on a timeline:
The blue boxes represent values that were emitted at a given time on the timeline, and the numbers represent the emitted values. A vertical line, like the one you see on the right-hand side of the diagram, represents a successful stream completion.
The simple contract of three possible events is so universal that it could represent any kind of dynamic data in your program. That’s why you can address any task in your app using Combine publishers — regardless of whether it’s about crunching numbers, making network calls, reacting to user gestures or displaying data on-screen.
Instead of always looking in your toolbox for the right tool to grab for the task at hand, be it adding a delegate or injecting a completion callback — you can just use a publisher instead.
One of the best features of publishers is that they come with error handling built in; error handling isn’t something you add optionally at the end, if you feel like it.
Publisher protocol is generic over two types, as you might have noticed in the diagram earlier:
Publisher.Outputis the type of the output values of the publisher. If it’s an
Intpublisher, it can never emit a
Publisher.Failureis the type of error the publisher can throw if it fails. If the publisher can never fail, you specify that by using a
When you subscribe to a given publisher, you know what values to expect from it and which errors it could fail with.
Operators are methods declared on the
Publisher protocol that return either the same or a new publisher. That’s very useful because you can call a bunch of operators one after the other, effectively chaining them together.
Because these methods, called “operators”, are highly decoupled and composable, they can be combined (aha!) to implement very complex logic over the execution of a single subscription.
It’s fascinating how operators fit tightly together like puzzle pieces. They cannot be mistakenly put in the wrong order or fit together if one’s output doesn’t match the next one’s input type:
In a clear deterministic way, you can define the order of each of those asynchronous abstracted pieces of work alongside with the correct input/output types and built-in error handling. It’s almost too good to be true!
As an added bonus, operators always have input and output, commonly referred to as upstream and downstream — this allows them to avoid shared state (one of the core issues we discussed earlier).
Operators focus on working with the data they receive from the previous operator and provide their output to the next one in the chain. This means that no other asynchronously-running piece of code can “jump in” and change the data you’re working on.
Finally, you arrive at the end of the subscription chain: Every subscription ends with a subscriber. Subscribers generally do “something” with the emitted output or completion events.
Currently, Combine provides two built-in subscribers, which make working with data streams straightforward:
The sink subscriber allows you to provide closures with your code that will receive output values and completions. From there, you can do anything your heart desires with the received events.
The assign subscriber allows you to, without the need of custom code, bind the resulting output to some property on your data model or on a UI control to display the data directly on-screen via a key path.
Should you have other needs for your data, creating custom subscribers is even easier than creating publishers. Combine uses a set of very simple protocols that allow you to be able to build your own custom tools whenever the workshop doesn’t offer the right one for your task.
Note: This book uses the term subscription to describe both Combine’s
Subscriptionprotocol and its conforming objects, as well as the complete chain of a publisher, operators and a subscriber.
When you add a subscriber at the end of a subscription, it “activates” the publisher all the way at the beginning of the chain. This is a curious but important detail to remember — publishers do not emit any values if there are no subscribers to potentially receive the output.
Subscriptions are a wonderful concept in that they allow you to declare a chain of asynchronous events with their own custom code and error handling only once, and then you never have to think about it again.
If you go full-Combine, you could describe your whole app’s logic via subscriptions and once done, just let the system run everything without the need to push or pull data or call back this or that other object:
Once the subscription code compiles successfully and there are no logic issues in your custom code — you’re done! The subscriptions, as designed, will asynchronously “fire” each time some event like a user gesture, a timer going off or something else awakes one of your publishers.
Even better, you don’t need to specifically memory manage a subscription, thanks to a protocol provided by Combine called
Both system-provided subscribers conform to
Cancellable, which means that your subscription code (e.g. the whole publisher, operators and subscriber call chain) returns a
Cancellable object. Whenever you release that object from memory, it cancels the whole subscription and releases its resources from memory.
This means you can easily “bind” the lifespan of a subscription by storing it in a property on your view controller, for example. This way, any time the user dismisses the view controller from the view stack, that will deinitialize its properties and will also cancel your subscription.
Or to automate this process, you can just have an
[AnyCancellable] collection property on your type and throw as many subscriptions inside it as you want. They’ll all be automatically canceled and released when the property is released from memory.
As you see, there’s plenty to learn, but it’s all logical when explained in detail. And that’s exactly what the plan is for the next chapters — to bring you slowly but steadily from zero to Combine hero by the end of this book.
What’s the benefit of Combine code over “standard” code?
You can, by all means, never use Combine and still create the best apps out there. There’s no argument about that. You can also create the best apps without Core Data,
URLSession, or even UIKit. But using those frameworks is more convenient, safe and efficient than building those abstractions yourself.
Combine (and other system frameworks) aim to add another abstraction to your async code. Another level of abstraction on the system level means tighter integration that’s well tested and a safe-bet technology.
It’s up to you to decide whether Combine is a great fit for your project or not, but here are just a few “pro” reasons you might not have considered yet:
- Combine is integrated on the system level. That means Combine itself uses language features that are not publicly available, offering you APIs that you couldn’t build yourself.
- Combine abstracts many common operations as methods on the
Publisherprotocol and they’re already well tested.
- When all of your asynchronous pieces of work use the same interface —
Publisher— composition and reusability become extremely powerful.
- Combine’s operators are highly composable. If you need to create a new one, that new operator will instantly plug-and-play with the rest of Combine.
- Combine’a asynchronous operators are already tested. All that’s left for you to do is test your own business logic.
As you see, most of the benefits revolve around safety and convenience. Combined with the fact that the framework comes from Apple, investing in writing Combine code looks promising.
As this question is most likely already sounding alarms in your head, take a look at how using Combine will change your pre-existing code and app architecture.
Combine is not a framework that affects how you structure your apps. Combine deals with asynchronous data events and unified communication contract — it does not alter, for example, how you would separate responsibilities in your project.
You can use Combine in your MVC (Model-View-Controller) apps, you can use it in your MVVM (Model-View-ViewModel) code, in VIPER and so forth and so on.
This is one of the key aspects of adopting Combine that is important to understand early — you can add Combine code iteratively and selectively, using it only in the parts you wish to improve in your codebase. It’s not an “all or nothing” choice you need to make.
You could start by converting your data models, or adapting your networking layer, or simply using Combine only in new code that you add to your app while keeping your existing functionality as-is.
It’s a slightly different story if you’re adopting Combine and SwiftUI at the same time. In that case, it really does make sense to drop the C from an MVC architecture. But that’s thanks to using Combine and SwiftUI in tandem — those two are simply on fire when in the same room.
View controllers just don’t have any chance against a Combine/SwiftUI team. When you use reactive programming all the way from your data model to your views, you don’t need to have a special controller just to control your views:
If that sounds interesting, you’re in for a treat, as this book includes a solid introduction to using the two frameworks together in Chapter 15, “In Practice: Combine & SwiftUI.”
In this book, you’ll start with the concepts first and move on to learning and trying out a multitude of operators.
Unlike other system frameworks, you can work pretty successfully with Combine in the isolated context of a playground.
Learning in an Xcode playground makes it easy to move forward and quickly experiment as you progress through a given chapter and to see instantly the results in Xcode’s Console:
Combine does not require any third-party dependencies, so usually, a few simple helper files included with the starter playground code for each chapter will suffice to get you running. If Xcode ever gets stuck while you experiment in the playground, a quick restart will likely solve the issue.
Once you move to more complex concepts than playing with a single operator, you’ll alternate between working in playgrounds and real Xcode projects like the Hacker News app, which is a newsreader that displays news in real time:
It’s important that, for each chapter, you begin with the provided starter playground or project, as they might include some custom helper code which isn’t relevant to learning Combine. These tidbits are pre-written so you don’t distract yourself from the focus of that chapter.
In the last chapter, you’ll make use of all the skills you learned throughout the book as you finish developing a complete iOS app that relies heavily on Combine and Core Data. This will give you a final push on your road to building real-life applications with Combine!
- Combine is a declarative, reactive framework for processing asynchronous events over time.
- It aims to solve existing problems, like unifying tools for asynchronous programming, dealing with mutable state and making error handling a starting team player.
- Combine revolves around three main types: publishers to emit events over time, operators to asynchronously process and manipulate upstream events and subscribers to consume the results and do something useful with them.
Where to go from here?
Hopefully, this introductory chapter has been useful and has given you an initial understanding of the issues Combine addresses as well as a look at some of the tools it offers to make your asynchronous code safer and more reliable.
Another important takeaway from this chapter is what to expect from Combine and what is out of its scope. Now, you know what you’re in for when we speak of reactive code or asynchronous events over time. And, of course, you don’t expect using Combine to magically solve your app’s problems with navigation or drawing on-screen.
Finally, having a taste of what’s in store for you in the upcoming chapters has hopefully gotten you excited about Combine and reactive programming with Swift. Upwards and onwards, here we go!