Swift Combine (Part 1 of 3) — From Rx to Combine

Fernando Putra
7 min readDec 20, 2023

--

Welcome to the first part of my Swift Combine series. In this opening chapter, we’ll explore the foundation of Combine, its use cases, benefits, and application architecture. Hopefully, this understanding will provide a solid base as we delve deeper into the world of Swift Combine in the subsequent chapters of this series.

Overview

The evolution of asynchronous programming took a significant leap with Microsoft’s introduction of Reactive Extensions in 2009. Open-sourced in 2012, it spurred the development of various Rx ports across multiple programming languages such as RxJS, RxKotlin, and RxDart, including the widely used RxSwift on Apple’s platforms.

However, a major transformation occurred in 2019 when Apple introduced native support for reactive programming: the Combine Framework. Combine is a declarative, reactive framework for processing asynchronous events over time. It aims to handle a wide range of asynchronous events, such as user interactions, network responses, scheduled events, and more. It also manages mutable states and makes error handling more collaborative.

Key features of the Combine framework include:

  • A declarative Swift API for processing values over time, which serves as a first-party alternative to popular frameworks like RxSwift and ReactiveSwift.
  • The ability to customize the handling of asynchronous events by combining event-processing operators.
  • Seamless integration with Apple’s frameworks, such as SwiftUI, which uses Combine for types like ObservableObject and Property Wrappers like @Published.
  • Debugging capabilities that simplify working with functional reactive languages, such as the use of print() to log all publishing events.

Foundation

In broad strokes, there are three core concepts in Combine: publishers, subscribers, and operators. These core concepts are essential to understanding and form the foundation of the Combine framework.

1. Publishers

Publishers are the declarative part of Combine’s API that can emit values over time to one or more subscribers, either synchronously or asynchronously. They initiate data streams and emit them to the subscribers. Regardless of the internal logic of the publisher — which can encompass various functionalities like mathematical calculations, networking, or handling user events — every publisher can emit multiple events of these three types:

  1. An output value of the publisher’s generic Output type.
  2. A successful completion.
  3. A completion with an error of the publisher’s Failure type.

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.

The Publisher protocol has two associated types and one key function:

  1. Publisher.Output: Type of the publisher’s output values. If it’s an Int publisher, it can never emit a String or a Date value.
  2. Publisher.Failure: Type of error the publisher can throw if it fails. If the publisher can never fail, specify that using a Never failure type.
  3. Publisher.Subscribe: Function that connects a subscriber to the publisher, allowing the subscriber to receive values from the publisher.

Here is an example of Publisher. Open the playground and add the following code:

In this code, you:

  1. Create a helper function example(of:) to encapsulate each example in a playground throughout the chapter.
  2. Create a Notification Name.
  3. Get a handle on the default Notification Center.
  4. Create a Publisher using the center’s publisher(for:object:) method, which emits an event when the default Notification Center broadcasts a notification.

If you were to post a notification now, the publisher wouldn’t emit it because there is no subscription to consume the notification yet.

2. Subscribers

Subscribers are the counterparts to publishers, who receive and react to data emitted by publishers. They listen to data streams and generally do “something” with the emitted output or completion events. Combine provides two built-in subscribers that make working with data streams straightforward:

  1. sink: Provide closures with your code that will receive output values and completions, allowing you to do anything with the received events.
  2. assign: Bind the resulting output to a property on your data model or a UI control, allowing you to display the data directly on-screen via a key path.

Here is an example of a Subscriber. Add the following code to print a message indicating that a notification was received.

In this code, you:

  1. Create a Publisher.
  2. Create a Subscription by calling sink on the Publisher.
  3. Post the notification.
  4. Cancel the Subscription.

The subscription will continue to receive as many values as the Publisher emits, which is known as unlimited demand, until it either completes, encounters an error, or is cancelled.

Note: The term Subscription is used to describe the connection of a Subscriber to a Publisher.

3. Operators

Operators are methods declared on the Publisher protocol, designed to modify values, add new values, remove existing values, or implement various other behaviours. Each operator creates and configures an instance of a Publisher or Subscriber, subscribing it to the publisher on which you invoke the method — referred to as the upstream — and forwarding the results to a Subscriber, known as the downstream.

This design allows them to avoid a shared state, focusing on working with the data they receive from the previous operator and providing their output to the next one in the chain. This ensures that no other asynchronously running piece of code can “jump in” and change the data you’re working on. Also, thanks to their high level of decoupling and composability, the operators can be combined to implement very complex logic while executing a single subscription. Much like puzzle pieces, they cannot be mistakenly arranged in the wrong order or fit together if the output of one doesn’t match the next one’s input type.

Here is an example of an Operator, one that you’ll become very familiar with when using Combine, called map(_:).

With this code, you:

  1. Create a Sequence Publisher that emits integers. (You will learn more in the next part).
  2. Use the map(_:) Operator to transform each emitted integer by squaring it.
  3. Subscribe to the result using the sink method, which prints the squared numbers.

Use Case

Combine has several use cases, including:

  • Real-time data streaming
    Combine excels in scenarios where real-time data updates are critical, such as financial applications. It enables seamless handling and processing of live market data, ensuring the user interface remains consistently up-to-date.
  • User interface updates
    In user interface development, Combine shines by facilitating direct binding of UI elements to data sources. This ensures that changes in the underlying data are automatically reflected in the user interface, providing a responsive and dynamic user experience.
  • Event-driven applications
    Combine proves advantageous in event-driven applications where various events trigger actions. Whether responding to user interactions, device events, or external triggers, Combine offers a clean and efficient way to handle asynchronous events, improving code readability and maintainability.
  • Reactive web services
    Combine simplifies handling asynchronous network requests and responses when working with web services. It allows developers to compose a series of operations for declarative data processing from web services, ensuring responsiveness in the application.

Combine Code vs. Traditional Code

While it’s possible to create the best apps without using Combine, leveraging this framework often provides convenience, safety, and efficiency compared to building such abstractions from scratch. Combine and similar system frameworks introduce an additional abstraction layer to asynchronous code. This system-level abstraction ensures tight integration, rigorous testing, and a reliable technology foundation.

Here are a few advantages of using Combine:

  • System-level Integration: Combine is integrated at the system level, using language features that are not publicly available. This grants access to APIs that you couldn’t replicate independently.
  • Tested Abstractions: Combine abstracts common operations as methods on the Publisher protocol, ensuring they are thoroughly tested and reliable.
  • Consistent Interface: With all asynchronous operations unified under the Publisher interface, the power of composition and reusability becomes highly potent.
  • Composable Operators: Combine’s operators are highly composable. Creating a new Operator seamlessly integrates with the existing Combine framework, enhancing flexibility.
  • Tested Asynchronous Operators: Combine’s asynchronous operators are rigorously tested, allowing you to focus on testing your business logic.

As an Apple-backed framework, Combine ensures safety, convenience, and a reliable development path. However, its suitability remains contingent on your project’s specific needs and goals.

Best Architecture for Combine

Combine, as a framework, doesn’t dictate how you structure your apps regarding architectural patterns. Its primary focus is handling asynchronous data events and providing a unified communication contract. Consequently, it doesn’t mandate a specific architectural paradigm, allowing flexibility in your design choices.

Whether you’re following the MVC, MVVM, VIPER, or any other architectural approach, Combine seamlessly integrates into your codebase. The key aspect is that adopting Combine doesn’t necessitate a complete overhaul of your architecture. Instead, you can incorporate Combine iteratively and selectively, enhancing specific parts of your codebase. This flexibility means you can introduce Combine gradually, starting with areas where you see the most benefit.

Whether it’s converting data models, adapting the networking layer, or selectively using Combine in new code while keeping existing functionality as-is, the choice is yours. It’s not an “all or nothing” decision, providing a pragmatic and incremental approach to adopting Combine in your project.

Congrats! You’ve learned a solid foundation of Combine, and now you’re ready to build upon it. In the next chapter, you’ll be exposed to practical examples and learn how to make network requests with Combine.

If you found this article helpful, kindly share it with your friends and follow my profile for future updates. Your support is much appreciated!

You can access the repository for this series below:

PS: I’m always welcome for pizza 🍕🍕🍕.

--

--