Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Requiring a Publisher with a current value

Tags:

combine

Both CurrentValueSubject and @Published.Publisher (retrieved via $ from an ObservableObject property) immediately send a notification with the current value when a new subscriber is added (verified with this example).

Is there a way to require this behaviour with a protocol?

For example, if you offer an initializer that requires to pass a publisher, one would use AnyPublisher here:

init(settings: AnyPublisher<Settings, Never>) {
    // ...
}

This would allow to be sneaky and pass in a PassthroughSubject erased to AnyPublisher. Is there a way to prevent this that would allow to pass in both a CurrentValueSubject or an @Published property? (something like a AnyValuePublisher?)

like image 851
Ralf Ebert Avatar asked Oct 17 '25 06:10

Ralf Ebert


2 Answers

There is no way to know up-ahead which Publisher will replay its latest value vs. which will not (since a custom publisher could do the same, too).

The only thing I can think of in this specific use case is a ghost protocol to only mark these two types for this use case:

protocol ReplayingPublisher: Publisher {}

extension CurrentValueSubject: ReplayingPublisher {}
extension Published.Publisher: ReplayingPublisher {}

struct MyObject {
    init<P: ReplayingPublisher>(publisher: P) {
        // P is only one of these two possible options
    }
}
like image 53
Shai Mishali Avatar answered Oct 21 '25 02:10

Shai Mishali


Below you will find implementation that is what you are looking for:

// MARK: - ValuePublisher

public protocol ValuePublisher: Publisher {
    var value: Output { get }
}

// MARK: - AnyValuePublisher

public struct AnyValuePublisher<Output, Failure>: ValuePublisher where Failure: Error {

    nonisolated public var value: Output {
        box.value
    }

    private let box: PublisherBoxBase<Output, Failure>

    public init<T: ValuePublisher>(_ valuePublisher: T) where T.Output == Output, T.Failure == Failure {

        if let erased = valuePublisher as? AnyValuePublisher<Output, Failure> {
            self.box = erased.box
        } else {
            self.box = PublisherBox(base: valuePublisher)
        }
    }

    nonisolated public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
        box.receive(subscriber: subscriber)
    }
}

// MARK: - AnyValuePublisher > PublisherBox

extension AnyValuePublisher {
    private class PublisherBoxBase<Output, Failure: Error>: ValuePublisher {
        internal var value: Output {
            fatalError("abstract method")
        }

        internal init() {}

        internal func receive<Downstream: Subscriber>(subscriber: Downstream)
            where Failure == Downstream.Failure, Output == Downstream.Input {
            fatalError("abstract method")
        }
    }

    private final class PublisherBox<PublisherType: ValuePublisher>: PublisherBoxBase<PublisherType.Output, PublisherType.Failure> {
        internal let base: PublisherType

        internal override var value: PublisherType.Output {
            base.value
        }

        internal init(base: PublisherType) {
            self.base = base
        }

        internal override func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
            base.receive(subscriber: subscriber)
        }
    }
}

extension Publisher where Self: ValuePublisher {
    public func eraseToAnyValuePublisher() -> AnyValuePublisher<Output, Failure> {
        AnyValuePublisher(self)
    }
}

extension CurrentValueSubject: ValuePublisher {}

Sample usage would be:

let publisher = CurrentValueSubject<Int, Never>(1).eraseToAnyValuePublisher()
let someValue: Int = publisher.value
like image 28
Michal Zaborowski Avatar answered Oct 21 '25 01:10

Michal Zaborowski



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!