swiftcombine

How can I get a Subscriber to subscribe to get only 4 elements from an array?


I am trying to implement a subscriber which specifies its own demand for how many elements it wants to receive from a publisher.

My code is below:

import Combine
 
var array = [1, 2, 3, 4, 5, 6, 7]
 
struct ArraySubscriber<T>: Subscriber {
    typealias Input = T
    
    typealias Failure = Never
    
    let combineIdentifier = CombineIdentifier()
    
    func receive(subscription: any Subscription) {
        subscription.request(.max(4))
    }
    
    func receive(_ input: T) -> Subscribers.Demand {
        print("input,", input)
        return .max(4)
    }
        
    func receive(completion: Subscribers.Completion<Never>) {
        switch completion {
            case .finished:
                print("publisher finished normally")
            case .failure(let failure):
                print("publisher failed due to, ", failure)
        }
    }
}
 
let subscriber = ArraySubscriber<Int>()
 
array.publisher.subscribe(subscriber)

According to Apple's documentation, I specify the demand inside the receive(subscription: any Subscription) method, see link.

But when I run this code I get the following output:

input, 1 
input, 2 
input, 3 
input, 4 
input, 5 
input, 6 
input, 7 
publisher finished normally

Instead, I expect the subscriber to only "receive" elements 1, 2, 3, 4 from the array.

How can I accomplish this?


Solution

  • The publisher not only asks you for your desired demand in receive(subscription:), but also after every time the subscriber receives an element, in receive(_:).

    Your code says you always want max 4 more elements after receiving every element:

    func receive(_ input: T) -> Subscribers.Demand {
        print("input,", input)
        return .max(4)
    }
    

    If you only ever want to receive 4 elements, you should return .none in this method.

    func receive(_ input: T) -> Subscribers.Demand {
        print("input,", input)
        return .none // <-----
    }
    

    The output now becomes:

    input, 1
    input, 2
    input, 3
    input, 4
    

    An alternative implementation is to keep a count of how many more elements you want, and always demanding 1 element each time. Example:

    var remainingElements = 4
    
    func receive(subscription: any Subscription) {
        subscription.request(.max(1))
    }
    
    func receive(_ input: T) -> Subscribers.Demand {
        print("input,", input)
        remainingElements -= 1
        return remainingElements > 0 ? .max(1) : .none
    }
    

    Depending on the publisher, the behaviour may or may not be different. For example, some publishers might eagerly produce more elements if you request .max(4) at the beginning.