3

Is there a way to create a CurrentValueSubject that is read-only?

So you could sink it publicly, read value publicly, but could only send values to it internally/privately. Want to use it in a library module.

Cristik
  • 30,989
  • 25
  • 91
  • 127
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
  • Is this https://developer.apple.com/documentation/combine/just what you want? or do you mean a value can change over time but not allow change via sending values? – Shadowrun Oct 15 '21 at 08:54
  • 1
    If you could not send values to it, what values would you read from it? – Sweeper Oct 15 '21 at 08:54
  • check [this out](https://stackoverflow.com/questions/61520841/swift-combine-turn-a-publisher-into-a-read-only-currentvaluesubject) same question on stack – Ahmed amin shahin Oct 15 '21 at 08:55
  • @Sweeper Thanks for pointing it out! I meant sending values to it internally / privately only (updated the question). – Geri Borbás Oct 15 '21 at 09:05
  • 3
    How about just `eraseToAnyPublisher`? The client code only sees a `Publisher`. – Sweeper Oct 15 '21 at 09:07
  • @Sweeper I think that won't have a `value` property. – Geri Borbás Oct 15 '21 at 09:29
  • @Ahmedaminshahin I think `@Published` implements `willSet` (https://stackoverflow.com/questions/60932494/swiftui-is-it-possible-to-get-didset-to-fire-when-changing-a-published-struct/63135268#comment115325395_63135268), so it is (may) not be suitable to `sink` manually. Also, I can't add `@Published` requirements to protocol definitions (which happens to be my use case). – Geri Borbás Oct 15 '21 at 09:30
  • I guess a custom publisher that wraps a `CurrentValueSubject` could just do this. – Geri Borbás Oct 15 '21 at 09:34
  • 1
    Who says you can't also expose a `value` property? Expose (only) as much as you need. That's encapsulation in a nutshell. (yes this is slowly converging to your `CurrentValueSubject` wrapper solution) – Sweeper Oct 15 '21 at 09:52
  • Related: [Swift Combine: turn a publisher into a read-only CurrentValueSubject](https://stackoverflow.com/q/61520841) – Cristik Oct 15 '21 at 10:18
  • 2
    Short answer: no. Long answer: there's no built in method to achieve that, you'll have to write some code for it. – Cristik Oct 15 '21 at 10:21
  • @Sweeper @Cristik Thanks for your inputs! According to the suggestions I ended up creating a wrapper `Publisher`. – Geri Borbás Oct 15 '21 at 12:22

4 Answers4

6

The best pattern is to have it declared private:

private let _status = CurrentValueSubject<ThisStatus?, Never>(nil)

and expose it through a computed property:

public var status: AnyPublisher<ThisStatus?, Never> {
    _status
        .eraseToAnyPublisher()
}
LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • 1
    Thanks, actually I just did that too, however, this way `value` is not accessible publicly. – Geri Borbás Oct 15 '21 at 12:05
  • I don't think I understand. Which value is not accessible publicly? – LuLuGaGa Oct 15 '21 at 13:06
  • 5
    Sorry, I meant [`CurrentValueSubject.value`](https://developer.apple.com/documentation/combine/currentvaluesubject/value). Especially a concern when you need the initialized value. – Geri Borbás Oct 15 '21 at 13:45
  • @GeriBorbás You could just add another computer property which gives read access to only value. – Darko Jan 26 '22 at 08:16
3

I ended up creating a Publisher wrapping a CurrentValueSubject.

This way it can be written internally (to the module), but other modules can only read/subscribe to the wrapping publisher.

public class ReadOnlyCurrentValueSubject<Output, Failure>: Publisher where Failure : Error {
    
    internal let currentValueSubject: CurrentValueSubject<Output, Failure>
    
    public internal(set) var value: Output {
        get { currentValueSubject.value }
        set { currentValueSubject.value = newValue }
    }
    
    public init(_ value: Output) {
        currentValueSubject = .init(value)
    }
    
    public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
        currentValueSubject.receive(subscriber: subscriber)
    }
}

With having internal access to the currentValueSubject the module can freely compose it internally, while the outside world can only consume the values.

Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
2

You can write a custom publisher that wraps a CurrentValueSubject, and that exposes the subject only at initialization time. This way, the code that creates the publisher is the only one having access to the subject, and is able to instruct the publisher to emit events.

The new publisher can look like this:

extension Publishers {    
    public struct CurrentValue<Value, Failure: Error>: Publisher {
        public typealias Output = Value
        public typealias Subject = CurrentValueSubject<Value, Failure>
        
        public var value: Value { subject.value }
        
        private var subject: Subject
        
        public static func publisher(_ initialValue: Value) -> (Self, Subject) {
            let publisher = Self(initialValue)
            return (publisher, publisher.subject)
        }
        
        private init(_ initialValue: Value) {
            subject = Subject(initialValue)
        }
        
        public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Value == S.Input {
            subject.receive(subscriber: subscriber)
        }
    }
}

, and can be consumed in this fashion:

class MyClass {
    // use this to expose the published values in a readonly manner
    public let publisher: Publishers.CurrentValue<Int, Never>

    // use this to emit values/completion
    private var subject: Publishers.CurrentValue<Int, Never>.Subject
    
    init() {
        (publisher, subject) = Publishers.CurrentValue.publisher(10)
    }
}

This way you have a readonly value publisher, and the only instance that can publish values is the one that instantiates the publisher.

Now, if the internal requirement you specified in the question must be taken ad-literam, then you can change the visibility of the CurrentValue.subject property to be internal, in this case you no longer need the static method.

Cristik
  • 30,989
  • 25
  • 91
  • 127
2

A good replacement in this case is:

@Published public private(set) var status: ThisStatus? = nil

Use $status to get the underlying publisher.

Darko
  • 9,655
  • 9
  • 36
  • 48