Combine is a brand-new framework which Apple released with iOS 13. Its key feature is that it allows you to write asynchronous code. In this blog, we will focus on its theoretical fundamentals and key concepts, accompanied by code examples. 

What is Combine?

Combine is Apple’s new reactive framework introduced at WWDC 2019. Official Apple’s documentation on Combine states the following: “Customize handling of asynchronous events by combining event-processing operators”. 

Another description of Combine framework is that it provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events such as user interface events, network responses, and other types of asynchronous data.

Those async events are viewed as data streams and with reactive programming, you observe these streams and react when a value is emitted. This approach allows us to write more readable and maintainable code while eliminating some techniques like nested closures and callbacks.

The key concepts of Combine Framework

Let’s start by explaining a few terms first:

  • A Publisher is an observable object that emits values over time
  • Objects that are listening to publisher are known as Subscribers
  • A Subject is a publisher that can be used to send new values to subscribers
  • Operators are built-in methods, that apply some kind of transformation to the data, and they can be chained one after another
  • A cancellable track’s a subscription to a publisher, and needs to be retained for our subscription to stay active.

As you can see, Combine framework comes with a few key concepts: 

  • Publisher and Subscriber
  • Operators
  • Subjects

By combining these elements we create streams of data that flow from input to output. Let’s move on to the first two key concepts, Publishers and Subscribers.

Publishers

A publisher is responsible for providing or requesting data. When we are describing a custom Combine publisher, we describe it with two associated types: one for output and one for failure. 

Subscribers

A subscriber is responsible for requesting and receiving the data provided by a publisher. A custom subscriber is described with two associated types, one for input and one for failure. When a subscriber subscribes to a desired publisher, it initiates the request for data, and controls the amount of data it receives.

Note that a publisher that has not had any subscription requests will not provide any data!

Some existing classes inside Apple’s Foundation framework have built in extensions to support working with Combine, such as operators for JSON decoding, publishers for NSNotificationNames and a URLSessionTask publisher that publishes the data response.

Also Combine provides us with two built in subscripts, assign(to: on:), and sink(receiveCompletion: receiveValue:) out of the box! 


So, let’s show this code in action through URLSessionTask publisher.

let url = URL(string: "https://www.samurai-digital.com/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.samurai-digital.com%2F")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)

struct SamuraiDigital: Codable {
    var title: String
    var url: String
    
    enum CodingKeys: String, CodingKey {
        case title
        case url = "provider_url"
    }
}

let cancellable = publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("Finished successfully")
        }
    }
, receiveValue: { response in
    let decoder = JSONDecoder()
    
    let repo = try? decoder.decode(SamuraiDigital.self, from: response.data)
    print(repo)
    //SamuraiDigital(title: "Digital Product Development Agency - Samurai Digital", url: "https://www.samurai-digital.com")
   //Finished successfully
})

In the example above, you can see that we created a publisher for a network request, and that we attached a built-in subscriber called sink, that we already mentioned in the article. Sink API lets us pass a closure that is being called whenever a new value is received, and also one that will be called once the publisher is completed. Notice that we stored subscriber in cancellable variable which can be manually cancelled. 

The above approach works perfectly fine, but we wrote our own code almost in the same manner as we write traditional closure-based code, by implementing logic into it. The power of Combine lies in chaining multiple operators on our data stream flow.

Check out how it works:

let url = URL(string: "https://www.samurai-digital.com/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.samurai-digital.com%2F")!
let cancellable = publisher.map(\.data)
                           .decode(type: SamuraiDigital.self, decoder: JSONDecoder.init())
                           .receive(on: DispatchQueue.main)
                           .sink(receiveCompletion: { completion in
                                switch completion {
                                    case .failure(let error):
                                        print(error)
                                    case .finished:
                                        print("Finished successfully")
                                }
                           }, receiveValue: { value in
                                print(value)  //SamuraiDigital(title: "Digital Product Development Agency - Samurai Digital", url: "https://www.samurai-digital.com")
 //Finished successfully
                           })

This brings us to the next key concept in the Combine framework, and that is Operators.

Operators

Operators are built in methods on Publisher that transform the publisher in some way and return another Publisher. The Operators can be chained one after another, and each operator in the chain receives a transformed publisher that has been returned by a previous one.

Subjects

A subject is a publisher that conforms to Subject protocol, and that protocol requires subjects to have a send(_:) function that can be used to send values from publisher to subscribers. In the Combine framework there are two types of built in subjects: CurrentValueSubject and PassthroughSubject. Both of them work in a similar way, when send(_:) is invoked they provide updated values to their subscribers. The difference is that CurrentValueSubject requires and remembers initial values, and PassthroughSubject doesn’t.

PassthroughSubject code example:

class GreetingService {
    
    let publisher = PassthroughSubject<String, Never>()
    
    private(set) var welcomeText: String = "Hello" {
        didSet {
            publisher.send(welcomeText)
        }
    }
    
    func replaceWelcomeText(with text: String) {
        welcomeText = text
    }
}

let service = GreetingService()

service.publisher.sink { value in
    print(value)
}

service.replaceWelcomeText(with: "Welcome to, ")
service.replaceWelcomeText(with: "Samurai-")
service.replaceWelcomeText(with: "Digital!")
service.publisher.send(completion: .finished)

//Welcome to,
//Samurai-
//Digital!
 class GreetingService {
    

CurrentValueSubject code example:

 let publisher = CurrentValueSubject<String, Never>("Hello")
    
    private(set) var welcomeText: String = "" {
        didSet {
            publisher.send(welcomeText)
        }
    }
    
    func replaceWelcomeText(with text: String) {
        welcomeText = text
    }
}

let service = GreetingService()

service.publisher.sink { (value) in
    print(value)
}

service.replaceWelcomeText(with: "Welcome to, ")
service.replaceWelcomeText(with: "Samurai-")
service.replaceWelcomeText(with: "Digital!")
service.publisher.send(completion: .finished)

//Hello
//Welcome to,
//Samurai-
//Digital!

Combine vs RxSwift

By looking at the concepts above we can clearly see similarities with another reactive framework that has been around since iOS 8.0, RxSwift. The image below shows main differences between the Combine framework and RxSwift.

Compatibility

When comparing the two frameworks, we can see that the Combine framework does not offer backward compatibility. That means it is not available for systems that have an operating system lower than iOS 13 and for macOS users lower than operating system Catalina. 

RxSwift has been available since iOS 8.0, so for RxSwift users, they won’t be switching sides anytime soon because there are no plans for introducing backward compatibility in the Combine framework.

Performance

Combine is a newer framework that has better performance and it’s a clear winner. Performance is five to ten times faster than its counterpart. Even though the RxSwift framework is optimized, you can not get that kind of level of optimization like Apple engineers can, because they use various optimization tools that are simply not available for  developers outside Apple.

UIKit 

In reactive frameworks we need a way of connecting our streams of data to our views and also the other way around, by obtaining streams of data from UIKit elements. For now, Combine does not support connecting to UIKit elements while RxSwift does, using the RxCocoa framework.

In the Combine framework we use the assign(to:,on:) method on the publisher instance to connect our stream of data to a view property. It can be done by passing the proper key path and a desired view.

Memory & Error management

Memory

Combine’s Cancellable protocol is used to provide a reference to a subscriber that allows the use of cancel. This is most typically used when you want a reference to a subscriber, to clean it up on deallocation. As soon as variable cancellable is deallocated, the subscription tied to it is automatically cleaned up. 

RxSwift does not have anything similar, instead it uses a Dispose bag. Dispose bag holds subscriptions (disposables) and it allows us to dispose of all our subscriptions at once. The addDisposableTo() operator is used to add a subscriber to the dispose bag which will call dispose() on each subscriber upon Dispose bag being deallocated.

Errors

There is a big difference when it comes to error management in these two frameworks. In Combine every Publisher needs to specify an error type in streams, while in RxSwift, Observables don’t use a specific error type.  They show any type of error when something goes wrong. Specification of error types in Combine, forces us to handle our errors properly. Something similar can also be done in RxSwift, by using Swift’s Result type.

NOTE – if your stream doesn’t throw any errors you can specify type Never for Failure. (Combine)

Conclusion – should you start working with Combine Framework?

Learning Combine framework will help you in the long term eventually, because it will be a must for reactive programming in SwiftUI, and that is the future. But for now, RxSwift isn’t going anywhere for at least a couple of years, mostly because Combine only supports systems newer than iOS 13+ and SwiftUI is somewhat still not ready for production. 

We see a lot of discussion on the internet, with questions such as Will Combine kill RxSwift? Our take on this is that, short term – no, long term – probably. These topics mentioned above are complex and we just scratched the surface, so we encourage you to start learning in this new reactive framework as soon as you can.

I hope you enjoyed the article and that you learned something new. Thanks for reading, and stay tuned for more stuff like this!