Publisher in Combine

Publisher in Combine

Continue with the Combine series, today we will discuss Publisher. Combine needs something that can model a data stream. This is the role of the Publisher protocol.

If you haven’t read my article about Combine in general, let’s check it out: Introduction to Combine in iOS | huypham85 (hashnode.dev)

Now we will focus on types of Publisher regularly used and how to use them

Publishers

Publisher from a Sequence

Fundamental data types in Swift

A data stream can be viewed as a sequence of events over time. Therefore a Sequence is a great source for a simple publisher. Like this example above, Array conforms to Sequence. Sequence has a property called publisher that creates a publisher that emits the element from the source sequence

let arrayPublisher = [1, 2, 3, 4, 5].publisher

Another example:

let stringPublisher = "Huy Pham".publisher

The initial value is a String, after transforming to a publisher, its emitted values are characters

The characteristic of this Publisher type is that it never makes errors, so the data type for Failure is Never

Publisher from transformation

We can create a new publisher by using transform operators

[1, 2, 3].publisher // publisher of integers
    .map(String.init) // now a publisher of strings
    .sink {
        print($0)
    }
    .store(in: &cancellable)

The map function takes the values emitted by the upstream publisher and passes them as input to the String.init function. So the result Publisher now emits a String rather than an Int

Besides, we can create publishers from other sealed struct of Publishers , be able to transform upstream data to your expected publisher. See the example below:

// combine latest operator
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
let combined = Publishers.CombineLatest(publisher1, publisher2)

// create a sequence by sequence operator 
let numbers = [1, 2, 3, 4, 5]
let sequencePublisher = Publishers.Sequence<[Int], Never>(sequence: numbers)

It’s we simulate the operators. I will have an article next week giving more detail about Combine’s operators.

Publisher from the Class’s property

@Published is a property wrapper that makes any property using it Publisher

This wrapper is class-constrained, meaning that you can only use it in instances of a class

class ViewModel {
    @Published var title = "the first title"
}

@Published doesn’t need to call send() method or access .value. When you directly change its value, it will update the value. Note that we’re using the dollar sign to access the projected value. If you’re not familiar with this technique, check Apple’s document about Property Wrapper and projected value

var cancellable = viewModel.$title.sink(receiveValue: { newTitle in
    print("Title changed to \\(newTitle)")
})
viewModel.title = "the second title"

// Prints:
// Title changed to the first title
// Title changed to the second title

This approach is very efficient with UIKit and SwiftUI in case you don’t want to change the architecture of your project

Just

Just struct creates a straightforward publisher that will emit one value and then complete

Just("A")

This is handy for use as the return type of a function or if you just want to emit just 1 value

Future

Future means a publisher that eventually produces a single value and then finishes or fails. It provides closure as Future.Promise has Result type parameter without a return value. In the successful case, the future downstream subscriber receives the element before the publishing stream finishes normally. If the result is an error, publishing terminates with that error.

func generateAsyncRandomNumberFromFuture() -> Future <Int, Never> {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let number = Int.random(in: 1...10)
            promise(Result.success(number))
        }
    }
}
cancellable = generateAsyncRandomNumberFromFuture()
    .sink { number in print("Got random number \\(number).") }

// Prints
// Got random number 9

Future can replace the callback of the function, which can allow you to express asynchronous behavior without deeply nested callbacks (callback hell)

Subject

PassthroughSubject

PassthroughSubject can be used to emit values to downstream subscribers. However, it doesn’t store or cache the most recent value

CurrentValueSubject

Like PassThroughSubject, it can emit values to downstream subscribers. However, unlike PassThroughSubject, CurrentValueSubject maintains and provides access to the most recent value it has received. When a new subscriber attaches to a CurrentValueSubject, it immediately receives the current value (if available) before getting any subsequent updates

import Combine

class DataManager {
    // Create a PassThroughSubject to emit updates
    let passThroughSubject = PassthroughSubject<String, Never>()

    // Create a CurrentValueSubject with an initial value
    var currentValueSubject = CurrentValueSubject<Int, Never>(0)

    func updateData(value: String) {
        passThroughSubject.send(value)
    }

    func updateCurrentValue(value: Int) {
        currentValueSubject.send(value)
    }
}

let dataManager = DataManager()

let passThroughSubscription = dataManager.passThroughSubject.sink { value in
    print("Received value from PassThroughSubject: \\(value)")
}

let currentValueSubscription = dataManager.currentValueSubject.sink { value in
    print("Received value from CurrentValueSubject: \\(value)")
}

// subjects emit data
dataManager.updateData(value: "Hello, World!")
dataManager.updateCurrentValue(value: 42)

// Prints
// Received value from PassThroughSubject: Hello, World!
// Received value from CurrentValueSubject: 0
// Received value from CurrentValueSubject: 42

The main difference between both subjects is that a PassthroughSubject doesn’t have an initial value or a reference to the most recently published element. Therefore, new subscribers will only receive newly emitted events.

  • A PassthroughSubject is like a doorbell push button When someone rings the bell, you’re only notified when you’re at home

  • A CurrentValueSubject is like a light switch When a light is turned on while you’re away, you’ll still notice it was turned on when you get back home.

Type Erasure

Sometimes you want to subscribe to the publisher without knowing too much about its details or the chain operators create complicated nested type

let publisher = Fail<Int, Error>(error: ErrorDomain.example)
    .replaceError(with: 0)
    .map { _ in "Now I am a string" }
    .filter { $0.contains("Now") }

The type of this publisher is:

Publishers.Filter<Publishers.Map<Publishers.ReplaceError<Fail<Int, Error>>, String>>

To manage these nested types publishers have the eraseToAnyPublisher() method. This is a form of “type erasure”. This method erases the complex nested types and makes the publisher appear as a simpler AnyPublisher<String, Never> to any downstream subscribers.

With AnyPublisher, can’t call send(_:) method, it’s important to wrap a subject AnyPublisher to prevent the view from sending events through it. When you use type erasure in the MVVM way, you can change the underlying publisher implementation over time without affecting existing clients.

Conclusion

In summary, Combine's Publishers provides iOS developers with a powerful solution for managing asynchronous events and data streams. Mastering Publishers is essential for creating robust, scalable iOS applications with reactive capabilities.


Thanks for Reading! ✌️

If you have any questions or corrections, please leave a comment below or contact me via my LinkedIn account Pham Trung Huy.

Happy coding 🍻