Combine is a reactive programming framework introduced by Apple in iOS 13, macOS 10.15, and other platforms. By leveraging a declarative approach, Combine helps handle event streams and data updates efficiently. One of the fundamental components in Combine are the Publishers. This article dives into what a Publisher is, how it works, and practical examples of its usage.
What is a Publisher?
A Publisher
is a type that can transmit a sequence of values over time. This allows to "listen" for changes that could have been done by an unknown element.
We can convert any type to a Publisher
type using the @Published
keyword, adding the possibility of subscribing to any new change.
Let's see an example where we have a ViewModel that subscribe to a Service property changes:
Model
struct Product {
let id: Int
let title: String
}
Service
class NetworkDataService {
@Published var products: [Product] = []
func fetchData() async {
do {
try await Task.sleep(nanoseconds: 1_000_000_000) // Simulates a network request
products = [
Product(id: 0, title: "First Product"),
Product(id: 1, title: "Second Product")
]
}
catch {
print("Error ocurred")
}
}
}
ViewModel
@Observable
class ViewModel {
var products: [Product] = []
let dataService: NetworkDataService = NetworkDataService()
var productsPublisher: AnyCancellable?
init() {
productsPublisher = dataService.$products.sink { products in
self.products = products
}
}
func onAppear() {
Task {
await dataService.fetchData()
}
}
}
In this example, the ViewModel subscribes to Service's products
changes using the .sink()
function and then triggers the Service load from onAppear()
function.
The main advantadge of this is that the ViewModel doesn't need to trigger products
load again in order to get new changes, but when there is any change in Service's products
, the it will be propagated to our ViewModel or to any other element that is subscribed to Service's products
.
dataService.products
will return the property value as usual, in this case an array of Product
.
dataService.$products
will return the publisher of the property.
AnyCancellable
As you may observed in the previous example, we assigned our publisher subscription (.sink()
) to a AnyCancellable
property. This is necessary in order to keep a reference and avoid to our subscription be deallocated, and it allows to cancel our subscriptions when we don't need them anymore.
There is another option to do this which is specially usefull when we subscribe to many Publisers at the same time. We can declare a Set
of AnyCancellable
and then store all the subscriptions there. This is commonly called "bag" in reactive programming.
...
var bag: Set<AnyCancellable> = Set()
init() {
dataService.$products.sink { products in
self.products = products
}
.store(in: &bag)
dataService.$comments.sink { comments in
self.comments = comments
}
.store(in: &bag)
}
...
PassthroughSubject
PassthroughSubject
acts as an event-emitting publisher that allows manual control over when and what values are published, this is an alternative to Published
keyword in case you need to have more control.
This will work as any other Publisher, but in this case we need to use the .send()
function when we want to emit a new value:
class NetworkDataService {
var productsPublisher: PassthroughSubject<[Product], NSError> = PassthroughSubject()
func fetchData() async {
do {
try await Task.sleep(nanoseconds: 1_000_000_000) // Simulates a network request
let products = [
Product(id: 0, title: "First Product"),
Product(id: 1, title: "Second Product")
]
productsPublisher.send(products)
}
catch {
print("Error ocurred")
}
}
}
As you may notised our Publisher can return an error, so apart from new values we can also send a completion (finish the subscription) with or without an error:
productsPublisher.send(products) // Sends new value
productsPublisher.send(completion: .failure(NSError(domain: "", code: 1))) // Finishes subscription with an error
productsPublisher.send(completion: .finished) // Finishes subscription without any error
This means that we need to manage those completions:
dataService.productsPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Finished with no error")
case .failure(let failure):
print("Finished with error: \(failure.localizedDescription)")
}
}, receiveValue: { products in
self.products = products
})
.store(in: &bag)
We have the possibility of declare a Publisher that never returns an error. In this case we are not obligated to subscribe to completion handlers:
var productsPublisher: PassthroughSubject<[Product], Never> = PassthroughSubject()
dataService.productsPublisher
.sink { products in
self.products = products
}
.store(in: &bag)
Be the first to comment