Skip to content

Instantly share code, notes, and snippets.

@twittemb
Created June 8, 2019 20:11
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twittemb/a7de9431d70d535a6c003b4ec753e10c to your computer and use it in GitHub Desktop.
Save twittemb/a7de9431d70d535a6c003b4ec753e10c to your computer and use it in GitHub Desktop.
import Combine
// When wrapping a value, we take advantage of the setter
// to feed a PassthroughSubject
@propertyWrapper
struct Published<T> {
private var innerValue: T
private let innerSubject = PassthroughSubject<T, Never>()
init(initialValue value: T) {
self.innerValue = value
}
var value: T {
get {
self.innerValue
}
set {
self.innerValue = newValue
self.innerSubject.send(self.innerValue)
}
}
private var asPublisher: AnyPublisher<T, Never> {
self.innerSubject.eraseToAnyPublisher()
}
}
// Since Published has an inner private `AnyPublisher` property,
// we can leverage it to make `Published` conform to the `Publisher` protocol
extension Published: Publisher {
typealias Output = T
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
self.asPublisher.receive(subscriber: subscriber)
}
}
// We can now declare a value `as a Published` value
@Published
var myValue: String = ""
// and listen to the changes (the returned Cancellable is ignored for the sake of clarity)
_ = $test.sink { value in
print ("Published value = \(value)")
}
myValue = "Swift 5.1"
myValue = "is"
myValue = "awesome"
// The result will be
"Published value = Swift 5.1"
"Published value = is"
"Published value = awesome"
@n8chur
Copy link

n8chur commented Jun 8, 2019

Thanks for sharing this!

Is the indirection involved with asPublisher and the innerValue necessary? Here's my WIP implementation:

@propertyWrapper
struct Published<Output>: Publisher {
    typealias Failure = Never

    private let subject: CurrentValueSubject<Output, Never>

    var value: Output {
        get { subject.value }
        set { subject.value = newValue }
    }
    
    init(initialValue: Output) {
        subject = CurrentValueSubject<Output, Never>(initialValue)
    }
    
    func receive<S>(subscriber: S) where S : Subscriber, Published.Failure == S.Failure, Published.Output == S.Input {
        subject.receive(subscriber: subscriber)
    }
}

@twittemb
Copy link
Author

twittemb commented Jun 8, 2019

Hi
You're right, the indirection is not necessary. I just found this more explicit since not every body knows that a Subject is also a Publisher. I wanted to make it clear that the subject was first exposed as a Publisher before forwarding the receive selector.

Your implementation is nice.

@n8chur
Copy link

n8chur commented Jun 8, 2019

I suppose there is one functional difference between our implementations—the use of a CurrentValueSubject will replay the current value to any subscribers that are attached.

I'm not sure which is the desirable behavior, but you could fix that with the CurrentValueSubject approach by changing the receive function to look like this:

    func receive<S>(subscriber: S) where S : Subscriber, Published.Failure == S.Failure, Published.Output == S.Input {
        subject
            .dropFirst()
            .receive(subscriber: subscriber)
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment