Combine I
Publishers
Combine I
Publishers
0
0
Checkbox to mark video as read
Mark as read

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.

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)

0 Comments

Join the community to comment

Be the first to comment

Accept Cookies

We use cookies to collect and analyze information on site performance and usage, in order to provide you with better service.

Check our Privacy Policy