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 homeA
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 🍻