Apple Combine Basics PART I

Apple Combine Basics PART I

Reactive programming is not something new but recently it gained much higher interest. The reason is simple: traditional imperative programming has some limitations when it comes to coping with the demands of today, where applications need to have high availability and provide low response times also during high load, that’s why we decided to take a look at Combine, apples new Reactive Programming framework.

And if you feel like this is the exact solution for your problems, contact us at Evolvice  and we will gladly explore the possibilities together with you.

 

Excited? Let’s dive deep into the world of reactive programming!

 

 

Imperative vs. Reactive programming

 

Reactive programming is a declarative programming paradigm. It is based on data streams and change propagation. In other words, you have some source of events (a stream), you observe that source and react when it emits a value or event. The source of events (e.g. event stream) can be any variable that change over time, any external events, like mouse clicks events, I/O events, http requests, etc.

Let’s take a look at an example:

a = b + c

In imperative programming (like Object Oriented programming) a depend on b and c in the moment of calculation of the expression. So, the machine will calculate b + c, assign the result to a. If afterwords b or c change, a will be not affected anymore. In reactive programming a depend on b and c until you break that dependency intentionally. It means, whenever b or c changes, a will react and change also (in our example it means a will be recalculated).

 

A bit of history…

 

The first ideas of reactive programming appeared in 1970s, but the first big splash was made with Microsoft’s introduction of Reactive Extension (Rx) for .NET. Rx tried to solve the problem of asynchronous data access from event based UI. The standard iterator pattern, where the client asks for data, is not applicable for asynchronous data access. To solve this problem, Rx changed the control flow using Observer pattern. So, the client, instead of requesting the data following the Iterator pattern simply waits for the data to be sent to the client, using the Observer pattern.

After Rx was introduced and successfully used for some domain problems solving as well as for binding the ViewModel to UI, reactive frameworks appeared for other programming language. For Objective-C there are some frameworks, like Bond and ReaciveCocoa, ReactiveObjc. For Swift probably SwiftRx is the leader of all reactive frameworks.

In 2019 Apple presented its own reactive framework – Combine. “Combine is a unified declarative framework for processing values over time.” – Apple sais. Even though they don’t define Combine as an RX framework, it still is.

 

 

Introduction to Combine

 

The heart of the Combine framework is the Observer pattern. Let’s recall how it works. The subject (also called Observable) can change its state and notify Observers about the change.

The Observable analog in Combine is called the Publisher, and the Observer – Subscriber. So, the subscriber subscribes to the Publisher. The publisher can emit some events/values, and the receiver receives those events/values and react to it.

Publishers are represented by a generic protocol Publisher

 

public protocol Publisher {
    associatedtype Output
    associatedtype Failure : Error

    func receive(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

 

 

A subscriber is represented by a generic protocol Subscriber

 

public protocol Subscriber : CustomCombineIdentifierConvertible {
   associatedtype Input
   associatedtype Failure : Error

   func receive(subscription: Subscription)
   func receive(_ input: Self.Input) -> Subscribers.Demand
   func receive(completion: Subscribers.Completion)
}

 

A publisher has only an Output (because it can only emit values), and a subscriber has only an Input (it can just receive events / values). Publisher also can emit a Failure value, as well as the subscriber, can receive it and react to it. We will talk about it a little bit more in the error handling section.

In order to connect a subscriber to a publisher they should have the same Input and Output interface, e.g. Publisher.Output and Publisher.Failure should be the same type as Subscriber.Input and Subscriber.Failure. This is described in the publisher protocol. There are some publishers and subscribers already implemented in Combine (you can also implement your own using the two protocols).

Let’s play with them in the Playground (do not forget to import Combine). We will make a very simple system made of a subscriber connected to a publisher. Call such a system “pipeline”.

 

import Cocoa
import Combine

let publisher = PassthroughSubject<Int, Never>()
publisher.sink { (value: Int) in
    print("received value (value)")
}

publisher.send(1)
publisher.send(5)

 

The output is:

received value 1

received value 5

 

PassthroughtSubject is a special kind of publishers, called the Subject.

PassthroughSubject<Int, Never>  means we created a publisher which can publish Int data, and no error (with the Never keyword).

sink – is one of the already implemented subscribers.

 

But what if we also want to handle errors? Lets first define some error enum

 

enum PublisherError: String, Error
 {
    case error1 = "error type 1"
    case error2 = "error type 2"
}

 

and change our publisher and subscriber.

 

let publisher = PassthroughSubject<Int, PublisherError>()
publisher.sink(receiveCompletion: { (completion: Subscribers.Completion) in
    
    switch completion {
    case .failure(let error):
        print("error: (error)")
    case .finished:
        print("Finished")
    }
    
}, receiveValue: { (value: Int) -> Void in
    print("received value (value)")
})

 

 

Because now the publisher is able to send errors, we need to define the receiveCompletion closure in the receiver. The completion argument is just an enum with 2 possible values: failure and success

 

When we run the playground, the output is

received value 1
received value 5
error: error2

 

 

Closing a Pipeline

 

As we see from the example above, after we created a pipeline we are able to send multiply events over it. So, the pipeline is not closed/destroyed after the first event arrived.

We can close the pipeline by sending Completion.finished or Completion.failure from the publisher. Study the next examples:

 

publisher.send(1)
publisher.send(5)
publisher.send(completion: .finished)
publisher.send(8)

 

Output

received value 1
received value 5
Finished

or with .failure

publisher.send(1)
publisher.send(5)
publisher.send(completion: .failure(.error1))
publisher.send(8)

 

Output

received value 1
received value 5
error: error1

After we send a completion, the pipe is closed, so, publisher.send(8) has no effect. Even if we try to subscribe to the closed publisher, it will not work again.

But what if we just want to cancel a subscription, and probably subscribe the publisher again some time later?

When we subscribe a publisher with sink, we get an instance of AnyCancellable which has a cancel() method for canceling a subscription.

 

let cancel = publisher.sink(receiveCompletion: { (completion: Subscribers.Completion) in
    
    switch completion {
    case .failure(let error):
        print("error: (error)")
    case .finished:
        print("Finished")
    }
    
}, receiveValue: { (value: Int) -> Void in
    print("received value (value)")
})

publisher.send(1)
publisher.send(5)
cancel.cancel()
publisher.send(8)

publisher.sink(receiveCompletion: {
    print($0)
}) {
    print("value ($0)")
}

publisher.send(10)

 

Output

received value 1
received value 5
value 10

 

 

 

Operators

 

Sometimes the emitted value from the Publisher needs to be processed before it gets to the receiver. We can accomplish it by Operators. An operator is a Publisher (because it has an Output) and a Subscriber (because it has an Input).

Let’s convert the Int value from our previous example to String before it gets to the receiver

 

let publisher = PassthroughSubject<Int, PublisherError>()
publisher
    .map{ (inputValue: Int) -> String in
        "(inputValue)"
    }
    .sink(receiveCompletion: { (completion: Subscribers.Completion) in
    
    switch completion {
    case .failure(let error):
        print("error: (error)")
    case .finished:
        print("Finished")
    }
    
}, receiveValue: { (value: String) -> Void in
    print("received value (value)")
})

publisher.send(1)
publisher.send(5)

 

Output

received value 1
received value 5

 

We can chain different operators. For example, let’s filter out odd numbers with filter Operator

 

let publisher = PassthroughSubject<Int, PublisherError>()
publisher
    .filter { (input: Int) -> Bool in
        input % 2 == 0
    }
    .map { (inputValue: Int) -> String in
        "(inputValue)"
    }
    .sink(receiveCompletion: { (completion: Subscribers.Completion) in
    
    switch completion {
    case .failure(let error):
        print("error: (error)")
    case .finished:
        print("Finished")
    }
    
}, receiveValue: { (value: String) -> Void in
    print("received value (value)")
})

publisher.send(1)
publisher.send(5)
publisher.send(6)

 

Output

received value 6

 

Combine has many operators, which can be categorized into:

  • map
  • filtering
  • reducing
  • mathematical operations on elements
  • applying matching criteria to elements
  • applying sequence operations to elements
  • selecting specific elements
  • combining elements from multiply publishers
  • handling errors
  • adapting publisher types
  • controlling timing
  • encoding and decoding
  • debugging

 

For more, refer to the official documentation

 

 

Error handling

 

Apple engineers made error handling in Combine explicit and type-safe (unlike other third party RX frameworks).

If an error can never happen, we can use Never to make it explicit (as we did in our first example).

 

 

Convert error types

 

Sometimes we need to connect a subscriber to a publisher, but they have different error types. For example, we have a search engine, which provides a publisher. Search engine publisher defines its own error type SearchError.  Every time the search engine gets a search request, it proceeds with it and sends the result via a publisher. Lets say, our UI code is the subscriber, which will show the search results somewhere in the window. It defines its own error type ServiceError.

 


import Cocoa
import Combine

enum SearchError: Error {
    case notFound
    case timeExpired
}

enum ServiceError: Error {
    case emptyResponse
    case buisy
}

let searchEnginePublisher = PassthroughSubject<Int, SearchError>()
let subscriber = Subscribers.Sink<Int, ServiceError>(receiveCompletion: { _ in }) { print("($0)") }

searchEnginePublisher.subscribe(subscriber)

 

If we run this code, we will get an error:

“Instance method ‘subscribe’ requires the types ‘SearchError’ and ‘ServiceError’ be equivalent”. To fix this, we need convert SearchError to ServiceError. In Combine we can do so with mapError operator, just like we used earlier the map operator to convert the publishers output type to the subscribers input type.

 

searchEnginePublisher
    .mapError { (error: SearchError) -> ServiceError in
        switch error {
        case .notFound:
            return .emptyResponse
        case .timeExpired:
            return .buisy
        }
    }
    .receive(subscriber: subscriber)

 

Lets try it:

 

searchEnginePublisher.send("hello")
searchEnginePublisher.send(completion: .failure(.notFound))
searchEnginePublisher.send("world")

 

Output

hello

 

As we see, after we sent the error, the pipe terminated. So, the string “word” is not sent to the receiver.

 

 

replaceError()

 

In our search engine example, the error handling strategy could be to use some placeholder value instead of propagating the error. So, we need to replace an error with a placeholder value. In our case we could show some “empty result” string. Combine provides us with replaceError(with: T) operator.

 

searchEnginePublisher
    .replaceError(with: "empty result")
    .receive(subscriber: subscriber)

searchEnginePublisher.send("hello")
searchEnginePublisher.send(completion: .failure(.notFound))
searchEnginePublisher.send("world")

 

Output

hello
empty result

 

Please note the next:

  • we replace ANY error value with a placeholder value (e.g. we can’t distinguish between different error types).
  • because any error values will be replaced, it means that the subscriber will never receive any error. So, we changed its interface from Subscribers.Sink<String, ServiceError> to Subscribers.Sink<String, Never>.
  • Please note, an error was emitted from the Publisher, so, the pipe will be closed.

 

 

Catch

 

In case we want to have some individual “placeholder” values for each error, we can use the Catch(error: Error) operator.

 

searchEnginePublisher
    .catch{ (error: SearchError) -> Just in
        switch error {
        case .notFound:
            return Just("Not found")
        case .timeExpired:
            return Just("Engine is busy. Try again")
        }
     }
    .receive(subscriber: subscriber)

searchEnginePublisher.send("hello")
searchEnginePublisher.send(completion: .failure(.timeExpired))
searchEnginePublisher.send("world")

 

Output

hello
Engine is busy. Try again

 

Please note the next:

  • Catch has an error argument, so we can replace a specific error value with some “placeholder” object (unlike replaceError, which doesn’t take any error argument).
  • because error values will be replaced, the receiver should have input error type Never.
  • the error was emitted from the Publisher, so, the pipe will be closed.

 

 

Producing errors

 

Sometimes it is needed to produce an error from an Operator in the pipeline. Here we go back to the initial version of our search engine example (without error replacing). If the publisher emits an empty string, it means no search result. So, instead of the empty string we expect the SearchError.notFound error. We can fix this by throwing the error in such a case. There are some operators with the “try” prefix in their names. All of those can produce an error by throwing it. We will use the tryMap operator.

 

searchEnginePublisher
    .tryMap { (value: String) throws -> String in
        guard !value.isEmpty else {
            throw SearchError.notFound
        }
        return value
    }
    .mapError { (error) -> SearchError in
        return error as! SearchError
    }
    .catch{ (error: SearchError) -> Just in
        switch error {
        case .notFound:
            return Just("Not found")
        case .timeExpired:
            return Just("Engine is busy. Try again")
        }
     }
    .receive(subscriber: subscriber)

searchEnginePublisher.send("hello")
searchEnginePublisher.send("")
searchEnginePublisher.send("world")

 

Output

hello
Not found

 

In the output we see only the first word “hello” and “Not found” – because we sent an empty string, which was converted to an error in the tryMap operator, and then converted to a “placeholder” string in cath operator.

Please note:

  • tryMap can throw only Error generic type. But our next operator in the pipe expect a SearchError.
  • We convert the Error (produced by the tryMap operator) to SearchError with the mapError operator.
  • When we throw an error – the pipe is terminated (despite the fact that the error was send by an Operator, not a Publisher).

 

 

Conclusion:

 

Congratulations! If you are still reading this article you’ve studied the basics of Combine, and now you are ready to write your amazing apps using this nice technology. We’ve covered all the components which compose the Combine framework: publishers, receivers, operators. And we’ve studied some error handling and the basic life cycle of the pipe. Also, we’ve seen how easy it is to set up a pipe.

In the next part, we will use our knowledge to create a complex pipe for interacting with a REST API. The interaction will be done in a few steps. Also, we will implement some error handling strategy.

Are you excited? Go to Apple Combine Intro Part II.