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.
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()
}
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()
There are a number of useful custom publishers that can be created using the Combine framework in Swift. Here are a few examples:
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.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.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.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.
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.