Swift Combine

Customise handling of async. events by combining event-processing operators.

As explained by Brian Advent and Sebastian Boldt .

  • Publisher

    • Defines how values and error are produced

    • Value type

    • Allows registration of Scubsriber

  • Subscriber

    • Receives values and a completion

    • Reference type

  • Operator

    • Adopts publisher

    • Describes a behavior for changing values

    • Subscribes to a publisher (upstream)

    • Send to a subscriber (downstream)

    • Value type

protocol Subscriber {
    associatedType Input
    associatedType Failure: Error
}

protocol Publisher {
    associatedType Output
    associatedType Failure: Error
}

| RXSwift | Combine | |:————–:|:——————:| | Observable | Publisher | | Observer | Subscriber | | Disposable | Cancellable | | PublishSubject | PassthroughSubject |

Arrays, Strings or Dictionaries can be converted to Publishers in Combine.

let helloPublisher = "Hello Combine".publisher
let fibonacciPublisher = [0,1,1,2,3,5].publisher
let dictPublisher = [1:"Hello",2:"World"].publisher

You subscribe to publishers by calling sink(receiveValue: (value -> Void))

let fibonacciPublisher = [0,1,1,2,3,5].publisher()_ = fibonacciPublisher.sink { value in
    print(value)
}
  • .finished will be emitted if the subscription is finished

  • .failure(_) will be emitted if something went wrong

let fibonacciPublisher = [0,1,1,2,3,5].publisher
_ = fibonacciPublisher.sink(receiveCompletion: { completion in
    switch completion {
        case .finished:
            print("finished")
        case .failure(let never):
            print(never)
    }
}, receiveValue: { value in
    print(value)
})

See playground

There are two types of subjects in combine.

  • PassthroughSubject all events after subscription.

  • CurrentValueSubject most recent element after subscription.

let passThroughSubject = PassthroughSubject<String, Error>()
passThroughSubject.send("Hello")
passThroughSubject.sink(receiveValue: { value in
    print(value)
})
passThroughSubject.send("World")

Will output World.

A simple example howto create a Publisher and a simple Subscriber can be found here and here.

let subject = CurrentValueSubject<String, Error>("Initial Value")
subject.send("Hello")
subject.send("World")
currentValueSubject.sink(receiveValue: { value in
    print(value)
})

Will output World.

Special Subscribers

  • sink(receiveCompletion:receiveValue:)

  • assign(to:on:)

Operators

| RXSwift | Combine | | :————–: | :——————: | | map | map,tryMap | | subscribe | sink | | do | handleEvents | | flatMap | flatMap |

See playground.

Map

Transform elements.

_images/operator_map.png

[1,2,3,4].publisher.map {
    return $0 * 10
}.sink { value in
    print(value)
}

Scan

Aggregate elements.

_images/operator_scan.png

[1,2,3,4].publisher.map {
    return $0 * 10
}.sink { value in
    print(value)
}

Filter

Filter elements.

_images/operator_filter.png

[2,30,22,5,60,1].publisher.filter{
    $0 > 10
}.sink { value in
    print(value)
}

FlatMap

Transform elements.

_images/operator_flatmap.png

// [Vadim Bulavin](https://www.vadimbulavin.com/map-flatmap-switchtolatest-in-combine-framework/)
print("flatMap")
struct User {
   let name: CurrentValueSubject<String, Never>
}

let userSubject = PassthroughSubject<User, Never>()
        
userSubject
.flatMap { $0.name }
.sink { print($0) }

let user = User(name: .init("User 1"))
userSubject.send(user)

Propertywrapper

  • @EnvironmentObject

  • @ObserverableObject

  • @State

  • @Published

  • @Binding

Example uses combine to set isenabled for a button, whenever canSendMessages (e.g. via UISwitch) is set to true.

class ViewController: UIViewController {
...

@Published var canSendMessages: Bool = false
private var switchSubscriber:AnyCancellable?

override func viewDidLoad(){
    super.viewDidLoad()
    self.setupProcessingChain()
}

func setupProcessingChain(){
    switchSubscriber = $canSendMessages.receive(on: DispatchQueue.main).assign(to: \isEnabled, on: sendButton)
}

@IBAction func didSwitch(_sender: UISwitch){
    canSendMessages = sender.isOn
}
...

Use NotificationCenter to send a message and combine to link that message to a UITextField.

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

struct Message {
    let content: String
    let author: String
}
...
class ViewController: UIViewController {
...

func setupProcessingChain(){
    switchSubscriber = $canSendMessages.receive(on: DispatchQueue.main).assign(to: \isEnabled, on: sendButton)
    let messagePublisher = NotificationCenter.Publisher(center: .default, name = .newMessage).map {notification -> String? in 
        return (notification.object as? Message).content ?? "" }
    let messageSubscriber = Subscribers.Assign(object: messageLabel, keyPath: \.text)
    messagePublisher.subscribe(messageSubscriber)
}
...
@IBAction func sendMessage(_ sender: Any){
    let message = Message(content: "The current time is \(Date())", author: "Me")
    NotificationCenter.default.post(name: .newMessage, object: message)    
}