Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine create a custom Subject

Tags:

swift

combine

I would like to have one Subject (similar to CurrentValueSubject) I can publish into, but that validates the value I'm sending. For example, I would like to validate that the input is forced between a range of values, like 1 and 10. If higher than the max, pass the maximum, if lower then the min, pass the minimum.

Please don't tell me to filter the result on the subscribing code because that's what I'm trying to avoid. That duplication.

Pseudo code would be:

let intSubject = ValidatedValueSubject<Int>(value: 5, min: 1, max: 10)

intSubject.sink { value in  print(value) }

intSubject.send(-10)
intSubject.send(5)
intSubject.send(15)

I would like this to produce:

5
1
5
10

Obviously with CurrentValueSubject I can't achieve that effect. I tried to create my own custom Subject but I can't seem to make it work.

Something tells me I should look at my problem differently because I guess this is too easy to need a custom Subject.

The use case:

I have a settings class which is updated on a Settings screen, and everywhere else, when the value change I want the screens to react accordingly. The ValidatedValueSubject lives inside this Settings object.

The Settings need to expose the Subject so any screens can react upon changes to the property.

My approach to the custom Subjectis as follows:

final class QualitySubject: Subject {

    public typealias Output = Int
    public typealias Failure = Never

    public private(set) var value: Output
    private let max: Output
    private let min: Output

    init(value: Output, max: Output, min: Output) {
        self.min = min
        self.max = max
        self.value = value
        self.value = validated(value)
    }

    private func validated(_ value: Output) -> Int {
        return max(min, min($0, max))
    }

    var subscription: [???? QualitySubscription ?????] = []

    public func send(_ value: Output) {
        self.value = validated(value)
        subscription.subscriber.receive(value)
    }

    public func send(completion: Subscribers.Completion<Failure>) {
        print("completion")
    }

    public func send(subscription: Subscription) {
        print("send subscription")
    }


    public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
        let qualitySubscription = QualitySubscription(value: value, subscriber: subscriber)
        subscriber.receive(subscription: qualitySubscription)

        // I think I should save a reference to the subscription so I could forward new values afterwards (on send method) but I can't because of generic constraints.
    }
}

like image 306
Nuno Gonçalves Avatar asked Oct 23 '25 19:10

Nuno Gonçalves


1 Answers

You can wrap a CurrentValueSubject:

class MySubject<Output, Failure: Error>: Subject {
    init(initialValue: Output, groom: @escaping (Output) -> Output) {
        self.wrapped = .init(groom(initialValue))
        self.groom = groom
    }

    func send(_ value: Output) {
        wrapped.send(groom(value))
    }

    func send(completion: Subscribers.Completion<Failure>) {
        wrapped.send(completion: completion)
    }

    func send(subscription: Subscription) {
        wrapped.send(subscription: subscription)
    }

    func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
        wrapped.subscribe(subscriber)
    }

    private let wrapped: CurrentValueSubject<Output, Failure>
    private let groom: (Output) -> Output
}

And use it like this:

let subject = MySubject<Int, Never>(initialValue: 5) { max(1, min($0, 10)) }
let ticket = subject.sink { print("value: \($0)") }
subject.send(-10)
subject.send(5)
subject.send(15)

Output:

value: 5
value: 1
value: 5
value: 10
like image 75
rob mayoff Avatar answered Oct 26 '25 12:10

rob mayoff