paint-brush
Useful Publishers in Swift: An Essential Guideby@vadimchistiakov
1,620 reads
1,620 reads

Useful Publishers in Swift: An Essential Guide

by Vadim ChistiakovJanuary 18th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

A publisher is a type that conforms to the `Publisher` protocol. It's responsible for providing a stream of values to subscribers. A publisher can emit one or more values over time, and it can also complete or fail. The Combine framework provides a number of built-in publishers.
featured image - Useful Publishers in Swift: An Essential Guide
Vadim Chistiakov HackerNoon profile picture

Overview

In the Combine framework, a publisher is a type that conforms to the Publisher protocol. It's responsible for providing a stream of values to subscribers. The Publisher protocol defines two associated types: Output and Failure, which indicate the types of the values that the publisher can emit and the types of errors that it can throw, respectively.


A publisher can emit one or more values over time, and it can also complete or fail. When a subscriber subscribes to a publisher, the publisher calls the subscriber's receive(subscription:) method and passes it a Subscription object, which the subscriber can use to control the flow of values. The subscriber can also call the receive(_:) method on the publisher to receive new values.


The Combine framework provides a number of built-in publishers, such as Just, Fail, Empty, Deferred, and Sequence, which can be used to create various types of publishers. Additionally, you can create your own custom publishers by conforming to the Publisher protocol and implementing the required methods.


Publishers can also be composed together to create more complex pipelines. The Combine framework provides a number of built-in operators that can be used to modify and combine publishers, such as map, filter, reduce, flatMap, zip, and merge. These operators are provided by the Publisher protocol extension and can be called on any publisher.


Now I would like to offer you some useful publishers which I use in my projects.

Repeating timer publisher

To implement a publisher that uses a repeating timer with a custom interval in Swift, you can use the Timer class from the Foundation framework. Here's an example of how you can do it:


RepeatingTimeSubscription conforms Subscription protocol:


private class RepeatingTimerSubscription<S: Subscriber>: 
                                         Subscription where S.Input == Void {
    private let interval: TimeInterval
    private let queue: DispatchQueue
    private var subscriber: S?
    private var timer: Timer?

    init(interval: TimeInterval, queue: DispatchQueue, subscriber: S) {
        self.interval = interval
        self.queue = queue
        self.subscriber = subscriber
    }

    func request(_ demand: Subscribers.Demand) {
        timer?.invalidate()
        timer = Timer.scheduledTimer(
            withTimeInterval: interval, repeats: true
        ) { [weak self] _ in
            self?.queue.async {
                _ = self?.subscriber?.receive()
            }
        }
    }

    func cancel() {
        timer?.invalidate()
        timer = nil
        subscriber = nil
    }
}


RepeatingTimePublisher conforms Publisher protocol:


import Foundation
import Combine

final class RepeatingTimerPublisher: Publisher {
    typealias Output = Void
    typealias Failure = Never

    private let interval: TimeInterval
    private let queue: DispatchQueue

    init(interval: TimeInterval, queue: DispatchQueue = .main) {
        self.interval = interval
        self.queue = queue
    }

    func receive<S>(subscriber: S) where S: Subscriber, 
                                            Failure == S.Failure, 
                                            Output == S.Input {
        let subscription = RepeatingTimerSubscription(
            interval: interval, 
            queue: queue, 
            subscriber: subscriber
        )
        subscriber.receive(subscription: subscription)
    }
}



To use this publisher, you can create an instance of it and subscribe to it using the sink method of the Publisher protocol.


For example:

private var cancellable: AnyCancellable?

func subscribeOnTimer(interval: TimeInterval) {
    let publisher = RepeatingTimerPublisher(interval: interval) 
    cancellable = publisher.sink { 
        print("Timer fired!") 
    }
}

//TEST THE METHOD
subscribeOnTimer(interval: 5.0)


This will print "Timer fired!" every 5 seconds. You can cancel the subscription by calling the cancel method on the AnyCancellable object that is returned by the sink method.


For example:


deinit {
    cancellable?.cancel()
}


Short Polling Publisher

To implement short polling using the Combine framework in Swift, you can create a publisher that makes a network request at a specified interval, and returns the response as the output. Here's an example of how you can do it:


Custom error enum for failed cases.

enum CustomError: Error {
    case invalidResponse
    case invalidDecoding
    case error
}


ShortPollingSubscription conforms Subscription protocol:


private class ShortPollingSubscription<S: Subscriber, Output: Decodable>:
    Subscription where S.Input == Output,
                       S.Failure == CustomError {
    
    private let url: URL
    private let interval: TimeInterval
    private let decoder: JSONDecoder
    private var subscriber: S?
    private var timer: Timer?
    private var task: URLSessionDataTask?

    init(
        url: URL,
        interval: TimeInterval,
        subscriber: S,
        decoder: JSONDecoder = JSONDecoder()
    ) {
        self.url = url
        self.interval = interval
        self.subscriber = subscriber
        self.decoder = decoder
    }

    func request(_ demand: Subscribers.Demand) {
        timer?.invalidate()
        timer = Timer.scheduledTimer(
            withTimeInterval: interval,
            repeats: true
        ) { [weak self] _ in
            self?.makeRequest()
        }
        makeRequest()
    }

    func cancel() {
        timer?.invalidate()
        timer = nil
        task?.cancel()
        task = nil
        subscriber = nil
    }

    private func makeRequest() {
        task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self else {
                return
            }
            if let error = error as? S.Failure {
                self.subscriber?.receive(
                    completion: .failure(error)
                )
                return
            }

            guard let data else {
                self.subscriber?.receive(
                    completion: .failure(.invalidResponse)
                )
                return
            }

            do {
                let output = try self.decoder.decode(
                    Output.self, 
                    from: data
                )
                _ = self.subscriber?.receive(output)
            } catch {
                self.subscriber?.receive(
                    completion: .failure(.invalidDecoding)
                )
            }
        }
        task?.resume()
    }
}


ShortPollingPublisher conforms Publisher protocol:


final class ShortPollingPublisher<Output: Decodable>: Publisher {
    
    typealias Failure = CustomError

    private let url: URL
    private let interval: TimeInterval

    init(url: URL, interval: TimeInterval) {
        self.url = url
        self.interval = interval
    }

    func receive<S>(subscriber: S) where S: Subscriber,
                                            Failure == S.Failure,
                                            Output == S.Input {
        let subscription = LongPollingSubscription(
            url: url,
            interval: interval,
            subscriber: subscriber
        )
        subscriber.receive(subscription: subscription)
    }
}


<Output: Decodable> means you can use any generic type of response that conforms Decodable protocol.


For testing you need to create a model that conforms Decodable. I use a public API by https://pixabay.com/api.


Let it be PhotoResponse struct:


struct PhotoResponse: Decodable {
    struct Photo: Decodable {
        let user: String
        let id: Int
        let largeImageURL: String
    }
    
    let hits: [Photo]
    let total: Int
}


To use this publisher, you can create an instance of it and subscribe to it using the sink method of the Publisher protocol. For example:


private var cancellable: AnyCancellable?

private func pollingTest() {
    let url = URL(string: "https://pixabay.com/api/?key={your_key}")!
    let publisher = ShortPollingPublisher<PhotoResponse>(
         url: url,
         interval: 5.0
    )
    cancellable = publisher.sink(receiveCompletion: { completion in
         switch completion {
         case .finished:
             print("Completed")
         case .failure(let error):
             print("Error: \(error)")
         }
     }, receiveValue: { response in
         print("Received response: \(response)")
     })
}

//TEST THE METHOD
pollingTest()


One more thing

There are a number of useful custom publishers that can be created using the Combine framework in Swift. Here are a few examples:

  1. NotificationCenterPublisher: A publisher that emits values when a specified notification is posted to the NotificationCenter. You can use this publisher to react to system or app-specific events, such as a device rotation or a network status change.
  2. KeyboardPublisher: A publisher that emits values when the keyboard is shown or hidden. You can use this publisher to adjust the layout of your views when the keyboard is presented or dismissed.
  3. CoreLocationPublisher: A publisher that emits values when the user's location changes. This publisher can be used to track the user's location and perform actions based on their location.
  4. UIControlEventPublisher: A publisher that emits values when a specified event occurs on a UIControl such as UIButton or UITextField. This can be used to handle user interactions in a reactive way.

These are just a few examples of the types of custom publishers that can be created using the Combine framework. The key is to understand the requirements of your application and to use the available building blocks provided by the framework to create publishers that meet those requirements.

In conclusion

Finally, it's important to note that the Combine framework uses the functional reactive programming paradigm, which is a programming model that deals with streams of events over time. Publishers, along with subscribers and operators, are the core building blocks of this paradigm, and they make it easy to create complex and responsive applications.