Using RxSwift for Reactive Programming in Swift

Swift has seen many changes since its release in 2014. It has become open-source and evolved a lot. Today’s Swift is a fast, modern, type-safe language, and it continues to become better with each major release. Moreover, when the Swift Standard Library can’t do something out of the box, the community can.

Nowadays, there are lots of third-party libraries written in Swift. RxSwift is one of them, and it’s definitely a big fish in the sea. This library brings to Swift the capabilities of functional reactive programming. With its large community and lots of support, RxSwift is a perfect choice for you to add some functional flavor to your code. That’s why we decided to write an introduction to reactive programming in Swift.

While Swift provides many benefits, the imperative nature of this language makes it hard to develop truly functional code. Why does one need functional code? Because it’s declarative, decomposed, and… fun!

In addition, functional code provides more benefits compared to the traditional imperative approach. Let’s dive into the differences between the imperative and functional approaches.

Read also: Why Is Swift Faster Than Objective-C?

Imperative-vs-Functional

Imperative vs functional

The imperative approach to programming entails a detailed description of each step the program should take to accomplish the goal. Machine code is written in the imperative style, and it’s a characteristic of most programming languages.

In contrast, a functional approach describes a set of functions to be executed. Each function transforms data in pure way, so the value returned by a function is only dependent on its input.

These two approaches to programming are very different.

Here are the main differences:

1. State changes

In pure functional programming, state changes do not exist, as there are no side effects. A side effect is when a state is modified in addition to a value being returned as the result of some external interaction. The referential transparency (RT) of a sub-expression is often referred to as having no side effects and is especially applicable to pure functions.

RT does not let a function access a mutable state that’s external to the function because every sub-expression is a function call by definition. Pure functions have the following attributes:

  • The only observable output is the return value
  • The only output dependency is the arguments
  • Arguments are fully determined before any output is generated

Though the functional approach minimizes side effects, it can’t avoid them completely, as they are the internal part of any code.

In contrast, functions in imperative programming lack referential transparency, which may be the only attribute that differentiates a declarative expression from an imperative. Side effects are widely used to implement state and perform I/O operations. Commands in the source language may change the state, which leads to the same expression resulting in different values.

What about Swift? This language provides value types that help to avoid state mutability. On the other hand, it has reference types, which may be mutated without problems. So Swift doesn’t restrict side effects or the use of mutable states. The same is true for RxSwift, a library written in Swift.

2. First-class citizens

In functional programming, objects and functions are first-class citizens. What does that mean? It means that functions can be passed as parameters, assigned to variables, and returned by other functions. Why is that useful? It makes it easy to manipulate execution blocks and compose and combine functions in various ways without any difficulties. For example: char *(*(**foo[][8])())[]; Have fun!

Languages that use the imperative approach have their own particularities regarding first-class expressions. The good news here is that Swift treats objects and functions as first-class citizens. The Swift Standard Library even includes useful higher-order functions like map and reduce. From this perspective, Swift is much more functional than its predecessor, Objective-C.

3. Primary flow control

Loops in the imperative style are represented as function calls and recursions in the functional style. Iteration in functional languages is usually accomplished via recursion. Why? For the sake of complexity, maybe. For us Swift (or maybe ex-Objective-C) iOS developers with our imperatively minded brains, loops seem much friendlier. Recursion can cause difficulties, like excessive memory consumption.

But… We can write a function without using loops or recursions! For each of the infinite possible specialized actions that could be applied to each element of a collection, Swift employs reusable iteration functions, such as map, flatMap, and filter. These functions are useful for refactoring code. They reduce repetition and don’t require you to write a separate function. (Read further: we have more on this!)

4. Order of execution

Declarative expressions express only the logical relationships of sub-expression function arguments and not mutable state relationships; due to the lack of side effects, the state transition of each function call is independent of others.

The functional order of execution for imperative expressions depends on their mutable state. That’s why the order of execution matters and is implicitly defined by the source code organization. Furthermore, we can point out the differences between the evaluation strategies of both approaches.

Lazy evaluation. This type of evaluation, referred to as call-by-need in functional programming languages, delays the evaluation of an expression until its value is needed and avoids repeated evaluations.The order of operations becomes indeterminate.

Eager evaluation. In contrast, imperative languages use eager evaluation. This means that an expression is evaluated as soon as it is bound to a variable. It also dictates the order of execution, making it easier to determine when sub-expressions (including functions) within the expression will be evaluated, which is important as these sub-expressions may have side effects that will affect the evaluation of other expressions.

5. Code size

Functional programming requires less code than imperative programming. That means fewer points of failure, less code to test, and a more productive programming cycle. This is especially important as a system evolves and grows.

Read also: JSON to Core Data

RxSwift Basics

Functional reactive programming is programming with asynchronous data streams. A data stream is just a sequence of values over time. According to the reactive programming idea, an application may be defined as a series of streams with operations that connect those streams. Each time anything happens in the app, the change appears as new data in some stream.
In imperative programming, you have to operate with existing values. This leads to the necessity of synchronizing asynchronous code and other difficulties. In contrast, in reactive programming, you work with streams.

Streams receive data that hasn’t been formed yet (in other words, asynchronous code is written in a synchronous way). Let’s switch to discussing the role of the Observable, the main object in RxSwift that represents data streams and plays the role of the Observer in receiving events.

Observable and Observer

Observable is the main object in RxSwift that represents data streams. Basically, it’s the equivalent to Swift’s Sequence, but it can receive elements asynchronously at some moment in time.

let helloSequence = Observable.just("Hello")
let alsoHelloSequence = 
Observable.from(["H","e","l","l","o"])

In order to handle events, we need one more element: an Observer. An Observer subscribes to Observable to receive events.

observers subscribing to observable

It’s important to note that an event won’t emit an RxSwift Observable until it has no subscribed Observers. This is not a bug; it’s an optimization feature.

An Observable may emit zero or more events over its lifetime, and it can be terminated normally or with an error.

There is an enumeration Event for all kinds of events in RxSwift:

– .next(value: T) – A new value is added to an observable sequence. This may be new data, a tap event on a button, etc.
– .error(error: Error) – If an error happens, a sequence will emit an error event and will be terminated.
– .completed – When a sequence ends normally, it emits a completed event.

let observable = Observable.of(“A”, “B”, “C”)
observable.subscribe { event in
    switch event {
    case .next(let value):
        print(value)
    case .error(let error):
        print(error)
    case .completed:
        print("completed")
    }
}	

Disposing

You can dispose of a subscription to cancel it and free resources:

let observable = Observable.just(["A", "B", "C"])
let subscription = observable.subscribe(onNext: { value in
    print(value)
})
subscription.dispose()

It’s possible to call dispose on each subscription individually, but it can be tedious. RxSwift allows you to put all subscriptions into DisposeBag, which will cancel them automatically in its deinit.

let disposeBag = DisposeBag()
let observable = Observable.just(["D", "E", "F"])
observable.subscribe(onNext: { value in
    print(value)
}).disposed(by: disposeBag)

 Read also: Web-based hosting service for software development projects

Examples of basic operations

Here are some diagrams showing how basic operations with RxSwift work:

rxswift merge

The result streams have both incoming streams of events merged together. As you can see, merge is useful whenever you don’t care about the particular source of events but would like to handle them in one place.

In our example, we have two independent data sources: local storage and network. We don’t care about the source and want to process received data in the same way:

let localDataObservable: Observable<Data> = ...
let networkDataObservable: Observable<Data> = ...
Observable.merge([localDataObservable, networkDataObservable])
    .subscribe(onNext: { data in
        // handle new data from any source here
    }).disposed(by: disposeBag)

CombineLatest

The result stream contains the latest values of the passed streams. If one of the streams has no value yet, the result will be zero.

rxswift combinelatest

When can we use this operator? It’s useful to enable the login button only when a user has entered the correct email and password, right? We can make that happen in the following way:

let isFormValidObservable = 
    Observable.combineLatest(emailTextField.rx.text, passwordTextField.rx.text)
        .map { email, password in
            return (email ?? "").isValidEmail() && (password ?? "").count > 3
        }
isFormValidObservable.bind(to: loginButton.rx.isEnabled).disposed(by: disposeBag)

Here, we’ve used the bind method from RxCocoa to connect values from the isFormValidObservable to the isEnabled` property of loginButton.

Filter

The result stream contains the values of the initial stream, filtered according to the given function.

rxswift filter

let filteredObservable = 
    Observable.of(0, 1, 2, 3, 4, 5)
    .filter { $0 >= 2 }

Now you may subscribe to this Observable to receive only filtered data from the stream.

Map

The value of the initial stream goes through the given f (x + 1) function and returns the mapped input value.

rxswift map

Let’s imagine you’re displaying a model’s title by applying some attributes to it. Map comes into play when “Applying some attributes” which is a separate function:

model.title
    .map { NSAttributedString(string: $0, attributes: ...) }
    .bind(to: titleLabel.rx.attributedText)
    .disposed(by: disposeBag)

Zip

The events of the result stream are generated when each of the streams has formed an equal number of events. The result stream contains the values from each of the two streams combined.

rxswift zip

Zip could be described for some use cases as a DispatchGroup. For example, say you’ve got three separate signals and need to combine their responses in a single point:

let contactsObservable =
    Observable.zip(facebookContactsObservable, addressBookContactsObservable)
        .map { facebookContact, addressBookContacts in
            let mergedContacts = ... // merge contacts somehow
            return mergedContacts
        }

Debounce

With a timer set to a definite time interval, the first value of the initial stream is passed to the result stream only when the time interval is over. If a new value is produced within the defined time interval, `debounce` ignores the first value and doesn’t let it be passed to the result stream. The second value appears in the result stream instead.

rxswift debounce

Amazing use case. Say we need to perform a search request when a user changes the searchField. A common task, huh? However, it’s not effective to construct and send network requests for each text change, since the textField can generate many such events per second and we’ll end up using the network inefficiently. The way out is to add a delay, after which we actually perform network requests. The usual way to achieve this is to add a Timer. With RxSwift, it’s much easier!

searchTextField.rx.text
    .debounce(.milliseconds(300), scheduler: MainScheduler.instance)
    .subscribe(onNext: { searchTerm in
        // initialize search flow here
    }).disposed(by: disposeBag)
 

Here, we used a MainScheduler instance. RxSwift uses the Scheduler pattern to abstract task execution, and MainScheduler executes tasks specifically on the main thread.

Delay

The value produced in the initial stream is delayed and passed to the result stream after a certain time interval.

rxswift delay

As a counterpart to debounce, delay will only delay the sending of “next”, “error,” and “completed” events.

textField.rx.text
    .delay(.milliseconds(500), scheduler: MainScheduler.instance)
    .subscribe(onNext: { text in
        // process text after delay
    }).disposed(by: disposeBag)

What we like about RxSwift

  • It introduces Cocoa bindings to iOS (with the help of RxCocoa).It allows you to compose operations on future data. (Read about futures and promises theory from Scala.)
  • It allows you to represent asynchronous operations in a synchronous way. RxSwift simplifies asynchronous software, such as networking code.
  • It offers convenient decomposition. Code that deals with user events and changing the app state may become very complex and spaghetti-like. RxSwift makes patterns of dependent operations particularly easy. When we represent operations like network response handling and user events as a stream of events that are combined in different ways, we can achieve high modularity and loose coupling, which leads to more reusable code.
  • Behaviors and relationships between properties are defined declaratively.
  • Minimizing shared states solves problems with synchronization. If you combine multiple streams, there’s one single place to handle all the results (whether it’s the next value, stream completion, or error).

At WWDC 2019, Apple introduced the Combine framework, which is an obvious competitor to RxSwift. By doing so, Apple has put its stamp of approval on functional reactive programming. The APIs in Combine are pretty similar to those in RxSwift. Check out a comparison cheat sheet that matches components and operators in RxSwift and Combine.

Combine is a native framework, and Apple supports it in its other frameworks like SwiftUI and RealityKit. This provides huge benefits compared to the third-party nature of RxSwift.

Combine (as well as SwiftUI) is available only starting with iOS 13, and there are not many applications ready to drop support for previous iOS versions just yet. But in a few years, we’ll be able to check trends within mobile app development and tell if Combine will completely replace third-party solutions for reactive programming in Swift.

For now, RxSwift is a great choice if you’re going to add reactiveness to your Swift code. It has a big community, lots of contributors, and great documentation. It looks hard to deal with at first sight (as does any reactive framework), but it’s totally worth it!

Ten articles before and after

How to Ensure Efficient Real-Time Big Data Analytics

Detailed Analysis of the Top Modern Database Solutions

Measuring Code Quality: How to Do Android Code Review

Android Studio Plugin Development

How to Quickly Import Data from JSON to Core Data

Which Javascript Frameworks to Choose in 2021

Best Tools and Main Reasons to Monitor Go Application Performance

How to Deploy Amin Panel Using QOR Golang SDK: Full Guide With Code

How to Speed Up JSON Encoding and Decoding in Golang

How to Use GitLab Merge Requests for Code Review