Web Analytics Made
Easy - StatCounter

Menu

About us Contact us

Step into Swift Combine

Tech 2019 / 08 / 28

Step into Swift Combine



What is Combine?

A unified declarative API for processing values over time.

Often in our code, we have many places where we have some sort of value or event Publisher and some Subscriber is interested in receiving values from that Publisher. And some interested party comes along and establishes a connection between these Publisher and Subscriber. Once the connection is established, the Subscriber sometimes declares that they are interested in receiving values from that Publisher (upstream), after which the Publisher is free to begin sending values to Subscriber (downstream). This goes on until either the Publisher decides to stop sending values, whether because it finished or there was some sort of failure, or by someone choosing to cancel the subscription. This general shape of communication appears throughout our software, whether it’s callbacks or closures or any other situations where there’s asynchronous communication. — Combine in Practice, WWDC 2019

This is the Pattern that Combine is all about, WWDC 2019

Key Concepts

Combine has following three concepts: PublishersSubscribers and  Operators.

Publishers

Publishers are the declarative part of Combine’s API which describe how values and errors are produced and allow registration of a Subscriber to receive these values over time. They specify two associatedTypes and a key function as shown below:

Publisher protocol. WWDC 2019

 

Outputis the type of value Publisher it produces.

Failureis the kind of errors that Publisher produces. If Publisher does not produce an error, then we can use type never for that associated type.

subscribe<S: Subscriber>(_ subscriber: S)is the key function that describes how to attach a Subscriber to a Publisher with the generic constraints that the associated types must match the Subscriber’s Inputto Publisher’s Output, and the Subscriber’s Failureto Publisher’s  Failure.

Here are some additional convenience publishers:

Just: It provides a single result and then terminates, it takes a failure type of <Never>

Future: A future is initialized with a closure that eventually resolves to a single output value or failure completion.

import Contacts

let futureAsyncPublisher = Future<Bool, Error> { promise in 
    CNContactStore().requestAccess(for: .contacts) { grantedAccess, err in 
        // err is an optional
        if let err = err { 
            promise(.failure(err))
        }
        return promise(.success(grantedAccess))
    }
}

Published: A property wrapper adds a Combine publisher to any property.

@Published var address: String = ""

$address
    .sink { someAddress in
        print("My address is: ", someAddress)
    }

$address
    .assign(\.text, on: addressLabel)

Published wraps the variable, address, and will trigger events whenever the variable is mutated. Any subscriber(s) subscribing to the property will also receive any initial value being set upon the property’s initialization.

The below example is the implementation of Publisher protocol:

import Combine

extension NotificationCenter {
    
    struct NotificationPublisher: Combine.Publisher {
        // implementing Publisher protocol
        typealias Output = Notification
        typealias Failure = Never
        
        let center: NotificationCenter
        let name: Notification.Name
        let object: Any?
        
        func receive<S>(subscriber: S) where S : Subscriber, 
            NotificationPublisher.Failure == S.Failure, 
            NotificationPublisher.Output == S.Input {
            // letting subscriber know subscription started
            subscriber.receive(subscription: Subscriptions.empty)

            // observing notification
            center.addObserver(forName: name, object: object, queue: nil) { (notification) in
                let _ = subscriber.receive(notification)
            }
        }
    }

    func publisher(for name: Notification.Name) -> NotificationPublisher {
        return Publisher(center: self, name: name, object: nil)
    }
}

let backgroundPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)

backgroundPublisher.sink { notification in 
    print("Entered in background")
}

Subscribers

Subscribers are the counterparts of Publishers. They receive values, including the completion if the Publisher is finite. As Subscribers usually act and mutate state upon receipt of values, so they are reference types in Swift; in other word, they are classes. Below is the protocol of Subscriber:

Subscriber protocol. WWDC 2019

 

Subscriber has two associatedTypes as shown above. One is Input and the other is Failure. Subscriber has three well-defined event functions to receive a subscription, values and a completion as described below:

  1. A Publisher will call receive(subscription: Subscription) just once in response to a subscribe call.
  2. A Publisher can then call receive(_ input: Input) to provide values to the Subscriber.
  3. Once the Publisher has finished or a failure has occurred it sends at most a completion signal and no further values are emitted by a Publisher once that  has been signalled.

There are two special Subscribers that takes burden from us:

sink(receiveCompletion:receiveValue:): Sink takes two closures. receiveCompletion executes upon subscribers completion and it is an enum that indicates whether the publisher finished normally or failed with an error and receiveValue triggers every time it receives an element from subscribed publisher.

enum WeatherError: Error {
    case invalidTemp
}

let weatherPublisher = PassthroughSubject<Int, WeatherError>()

let subscriber = weatherPublisher
    .filter { $0 >= 25}
    .sink(receiveCompletion: { error in
        print("Subscription completed with rrror: \(error)")
    }) { value in
        print("Summer day of \(value) ℃")
}

let anotherSubscriber = weatherPublisher
    .handleEvents(receiveSubscription: { subscription in
        print("New subscription: \(subscription)")
    }, receiveOutput: { value in
        print("New value: \(value)")
    }, receiveCompletion: { error in
        print("Subscription completed with rrror: \(error)")
    }, receiveCancel: {
        print("Subscription cancelled")
    }) { request in
        print("New request \(request)")
    }

weatherPublisher.send(20)
weatherPublisher.send(35)
weatherPublisher.send(completion: Subscribers.Completion<WeatherError>.failure(.invalidTemp))
weatherPublisher.send(200)

assign(to:on:): It immediately assigns every element it receives to a property of a given object using key path to indicate the property

import UIKit
import Combine

extension Notification.Name {
    static let newBlogPost = Notification.Name("new_blog_post")
}

struct BlogPost {
    let title: String
    let url: URL?
}

let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)
    .map { (notification) -> String? in
        return (notification.object as? BlogPost)?.title ?? ""
    }

let blogPostLabel = UILabel()
let blogPostSubscriber = Subscribers.Assign(object: blogPostLabel, keyPath: \.text)
blogPostPublisher.subscribe(blogPostSubscriber)

let blogPost = BlogPost(title: "Step into Swift Combine", url: URL(string: "https://monstar-lab.com/global/"))
NotificationCenter.default.post(name: .newBlogPost, object: blogPost)
print("Last post is: \(blogPostLabel.text!)")

Operator

While working with Combine we often come to a scenario where output of the Publisher does not match the input of the Subscriber. Operators are the real saviors for such cases. They subscribe to Publishers (upstream), describe a behavior for changing values and then send the result to a Subscriber (downstream). Based on actions, Operators can be divided into the three following types:

1. Transformation Operators:

map: it operates just like Swift standard library map, except it operates on Publishers.

Marble diagram of map

 

flatMap: it tears down an observable sequence where elements of the observable itself are observables and merges the resulting observable sequences into one observable sequence. flatMap is mostly used in error handling scenarios.

Marble diagram of flatMap

 

2. Combination Operators:

merge: it combines multiple Observables into one by merging their elements

Marble diagram of merge

combineLatest: it combines the latest elements from two or more Publishers when any one of the Publishers emits an element.

Marble diagram of combineLatest

 

 

zip: this is similar to Combine Latest but unlike Combine Latest Zip waits for each Publisher to emit new elements.

Marble diagram of zip

 

3. Filtering and other Operators:

There are a lot more operators like filter removeDuplicates, debounce, count, contains and more which are self explanatory. I encourage you to go to Apple’s documentation and try some yourself.

Lot more Operators, WWDC 2019

 

Subject

Subjects are special types of Publisher in Combine in which we can subscribe as well as send events to them dynamically.

Lot more Operators, WWDC 2019

 

As shown in the above image, Subject describes two functions. One is to send value and other is to send completion with failure.

Combine provides two very handy subjects:

PassthroughSubject: It sends every element after subscription.

・CurrentValueSubject: It behaves same as the PassthroughSubject but unlike PassthroughSubject, CurrentValueSubject stores the most recent elements and send to new subscribers.

Scheduler

receive: it takes a single required parameter (on:) which accepts a scheduler, and an optional parameter (optional:)

examplePublisher.receive(on: RunLoop.main)

subscribe: Subscribe defines the scheduler on which to run a publisher in a pipeline.

networkDataPublisher     
.subscribe(on: backgroundQueue)  // 1 
.receive(on: RunLoop.main)  // 2
.assign(to: \.text, on: nameLabel)  // 3
  1. The subscribe call requests the publisher (and any pipeline invocations before this in a chain) be invoked on the backgroundQueue.
  2. The receive call moves the data to the main thread to update the UI elements
  3. The assign call uses the assign subscriber to bind data to a KVO compliant object.

AnyCancellable

AnyCancellable is used to keep the reference of a subscriber so that we can clean up the subscriber on deallocation.

var cancellable: AnyCancellable?
let sinkSubscriber = aPublisher
    .sink { data in
        print("received ", data)
    }
cancellable = AnyCancellable(sinkSubscriber)

Source Code:

ー rokonuddin/CombineBook

Related Articles:

Using Combine

Problem Solving with Combine Swift

Getting started with the Combine framework in Swift

RxMarbles

Conclusion

Combine is a very powerful framework, introduced in Swift 5.1. It enables developers to dive into reactive programming. Apple ingeniously designed this language feature which goes hand in hand with Apple’s new declarative UI framework SwiftUI yet keeping UIKit unchanged.

Though Combine itself is a huge topic to write on, in this article I have provided explanations and covered the basics to start with.

Later I have tried to cover most commonly used keywords in Combine with diagrams and code snippets.

Finally, I shared a working example of Combine with SwiftUI.

I hope that you have enjoyed this article. I encourage you to read my other articles on Swift Type ErasureCustom UIView from .xib and TableView Prefetching DataSource using Swift

Thank you all for your attention 🙏🏻. Feel free to tweet and get connected.


Related Posts :

iOS TableView Prefetching DataSource using Swift

Swift: From Protocol to AssociatedType then Type Erasure

You have ideas, We have solutions.

CONTACT US!