Swift Combine —Create Your Own Custom Subscriber
In the previous article, we have learnt about default protocols of ‘Publisher’, ‘Subscriber’ and ‘Subscription’. Many times we need our custom subscriber according to our use case need. In this article, we will explore how we can create our own subscriber by adopting the default ‘Subscriber’ protocol.
If you are directly coming to this article, basic knowledge of subscriber, publisher and subscription is expected. If you wish to revise feel free to revisit the learning series.
Let’s say we want to create our own OrderNumber subscriber which monitors the emitted order number according to our use case. Let’s jump on the xcode playground and add below code.
Remember, we are going to see practical coding of all steps from our subscription diagram of our previous article. Let’s reload it into our mind memory.
Create a ‘OrderNumberSubscriber’ subscriber class which conforms to ‘Subscriber’ protocol. Add all required methods of that protocol. Xcode will ask you to define Input and Failure type, we will add Int for Input and Never for failure. We can make this class ‘final’, if we want to prevent inheritance from this subscriber.
Subscribers.Demand
When subscriber receives value from the Publisher in func receive(_ input: Int) -> Subscribers.Demand method, it asks for the future need of elements as a Demand.
On the receiving each value, Subscriber can decide and adjust its demand for the future event. Managing and Adjusting demand dynamically is known as ‘Backpressure Management’. This helps a subscriber to not get over flooded with the values from the publisher more than it can handle or wanted.
Widely used ‘Subscribers.Demand’ are:
- unlimited : This demand informs publisher that Subscriber is still ready to handle unlimited events.
- none : This demand informs publisher that Subscriber is not interested to adjust its demand. Just stick with its original asked demand of the events.
- max(value: Int) : This informs publisher to adjust previously asked demand with new value. Value passed in demand must be positive to avoid fatal error. Once subscriber adjust its demand, new demand will be increased from the previously asked demand. This implies that you can raise the initial max with each new value received, however, it cannot be lowered.
Let’s continue our coding example and add some code.
import Foundation
import Combine
/*
This is a Util function for logs.
Function will take a title of the topic and will execute the blocks
after printing.
*/
public func myLearningCode(of title: String,
execute: () -> Void) {
print("\n ***** Example of:", title, "***** \n")
execute()
}
// 1
myLearningCode(of: "Custom Subscriber") {
// 2
class OrderNumberSubscriber: Subscriber {
// 3
typealias Input = Int
// 4
typealias Failure = Never
// 5
func receive(subscription: Subscription) {
subscription.request(.max(5))
}
// 6
func receive(_ input: Int) -> Subscribers.Demand {
print("received a value \(input)")
return .none
}
// 7
func receive(completion: Subscribers.Completion<Never>) {
print("received a completion \(completion)")
}
}
}
- Our helper function to log example title into console.
- Created a class ‘OrderNumberSubscriber’ which conforms the default ‘Subscriber’ protocol.
- Defined Input type our custom subscriber is willing to receive.
- Defined Failure type for our custom subscriber. User ‘Never’ which suggest subscriber won’t receive any error.
- This is a callback from the Publisher with the created subscription instance. Subscriber asks for the initial demand via ‘request’ method. Map this with step #2 from the Publisher side and step #3 from the subscriber side of our above mentioned communication sequence diagram.
- Gets called every time publisher emits the event. Our subscriber will get those events in this callback. Subscriber can adjust future demand according to need.
- Defines the completion callback from the Publisher.
Next step is to create a Publisher for it. We will use simple Integer range pubshiber for this example.
// 1
myLearningCode(of: "Custom Subscriber") {
// 2
class OrderNumberSubscriber: Subscriber {
// 3
typealias Input = Int
// 4
typealias Failure = Never
// 5
func receive(subscription: Subscription) {
subscription.request(.max(5))
}
// 6
func receive(_ input: Int) -> Subscribers.Demand {
print("received a value \(input)")
return .none
}
// 7
func receive(completion: Subscribers.Completion<Never>) {
print("received a completion \(completion)")
}
}
// 8
let orderPublisher = (1...10).publisher
// 9
let orderSubscriber = OrderNumberSubscriber()
// 10
orderPublisher.subscribe(orderSubscriber)
}
Let’s run and see the output.
Wait..!! Why we haven’t received a completion call from the Publisher? !!!
Because, we have demanded max(5) events in our step #5 while creating the initial demand, but Publisher do have more events.
Let’s try to adjust our demand to unlimited (step #6)on receiving each value and see.
// 1
myLearningCode(of: "Custom Subscriber") {
// 2
class OrderNumberSubscriber: Subscriber {
// 3
typealias Input = Int
// 4
typealias Failure = Never
// 5
func receive(subscription: Subscription) {
subscription.request(.max(5))
}
// 6
func receive(_ input: Int) -> Subscribers.Demand {
print("received a value \(input)")
// 11
return .unlimited
}
// 7
func receive(completion: Subscribers.Completion<Never>) {
print("received a completion \(completion)")
}
}
// 8
let orderPublisher = (1...10).publisher
// 9
let orderSubscriber = OrderNumberSubscriber()
// 10
orderPublisher.subscribe(orderSubscriber)
}
11. On receiving the event, subscriber will still get chance to adjust its initial events demand from the Publisher for the future events. We are using unlimited demand which is telling publisher to send as much as possible events after the current received one.
Run the code and you must receive now completion event.
Try changing ‘.unlimited’ demand to ‘.max(2)’ at step #11, you will see same output because subscriber is increasing its demand by 2 every time it is receiving the value.
Try playing around this demand change and see the difference in output to understand this in depth. Also, try to comment ‘subscription.request(.max(5))’ line (step #5) and see what is the output.
We have understood the creation and working of a Custom Subscriber and its communication callbacks from the Publisher. We will head towards exploring ‘Future’ in our next article. Stay tuned with the combine learning series.
Feel free to follow me to stay updated with the upcoming articles.
Thank You. Cheers!
#Pre-Requisite
Intermediate level of the swift language / iOS is required for this Combine Article series.
#Credits