Skip to content

Instantly share code, notes, and snippets.

@sharplet
Last active July 31, 2025 20:52
Show Gist options
  • Select an option

  • Save sharplet/14703e2988e38d870198064989263f75 to your computer and use it in GitHub Desktop.

Select an option

Save sharplet/14703e2988e38d870198064989263f75 to your computer and use it in GitHub Desktop.
// Copyright (c) 2019–25 Adam Sharp and thoughtbot, inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Combine
/// Declares a type that can transmit a sequence of values over time, and
/// always has a current value.
public protocol CurrentValuePublisher<Output, Failure>: Publisher {
/// The current value of this publisher.
var value: Output { get }
}
// MARK: Conforming Types
extension CurrentValueSubject: CurrentValuePublisher {}
extension Just: CurrentValuePublisher {}
extension Published.Publisher: CurrentValuePublisher {}
// MARK: Public
extension CurrentValuePublisher where Failure == Never {
@inlinable
public var value: Output {
_getValue()
}
}
extension CurrentValuePublisher {
/// Subcribes to an additional `CurrentValuePublisher` and publishes a tuple
/// upon receiving output from either publisher. Additionally provides access
/// to a tuple of each publisher's current values.
///
/// For details about how the returned publisher handles demand requests and
/// failures, see `Publisher.combineLatest(_:)`.
///
/// - Parameter other:
/// Another `CurrentValuePublisher` to combine with this one.
public func combineLatest<P: CurrentValuePublisher>(_ other: P) -> some CurrentValuePublisher<(Output, P.Output), Failure>
where P.Failure == Failure
{
combineLatest(other) { ($0, $1) }
}
/// Subcribes to an additional `CurrentValuePublisher` and upon receiving
/// output from either publisher, publishes the result of applying `transform`
/// to the values. Additionally provides access to the current transformed
/// value.
///
/// For details about how the returned publisher handles demand requests and
/// failures, see `Publisher.combineLatest(_:)`.
///
/// - Parameter other:
/// Another `CurrentValuePublisher` to combine with this one.
public func combineLatest<P: CurrentValuePublisher, NewOutput>(
_ other: P,
_ transform: @escaping (Output, P.Output) -> NewOutput
) -> some CurrentValuePublisher<NewOutput, Failure>
where P.Failure == Failure
{
let combined = eraseToAnyPublisher().combineLatest(other, transform)
return ReadOnlyCurrentValuePublisher(
unsafeSubject: combined,
value: combined._getValue,
transform: { $0 }
)
}
/// Subcribes to two additional `CurrentValuePublisher` and publishes a tuple
/// upon receiving output from any publisher. Additionally provides access
/// to a tuple of each publisher's current values.
///
/// For details about how the returned publisher handles demand requests and
/// failures, see `Publisher.combineLatest(_:_:)`.
///
/// - Parameter publisher1:
/// Another `CurrentValuePublisher` to combine with this one.
public func combineLatest<P: CurrentValuePublisher, Q: CurrentValuePublisher>(_ publisher1: P, _ publisher2: Q) -> some CurrentValuePublisher<(Output, P.Output, Q.Output), Failure>
where P.Failure == Failure, Q.Failure == Failure
{
combineLatest(publisher1, publisher2) { ($0, $1, $2) }
}
/// Subcribes to two additional `CurrentValuePublisher`s and upon receiving
/// output from any publisher, publishes the result of applying `transform` to
/// the values. Additionally provides access to the current transformed value.
///
/// For details about how the returned publisher handles demand requests and
/// failures, see `Publisher.combineLatest(_:_:)`.
///
/// - Parameter publisher1:
/// Another `CurrentValuePublisher` to combine with this one.
public func combineLatest<P: CurrentValuePublisher, Q: CurrentValuePublisher, NewOutput>(
_ publisher1: P,
_ publisher2: Q,
_ transform: @escaping (Output, P.Output, Q.Output) -> NewOutput
) -> some CurrentValuePublisher<NewOutput, Failure>
where P.Failure == Failure, Q.Failure == Failure
{
let combined = eraseToAnyPublisher().combineLatest(publisher1, publisher2, transform)
return ReadOnlyCurrentValuePublisher(
unsafeSubject: combined,
value: combined._getValue,
transform: { $0 }
)
}
/// Transforms the current value and all future values of the upstream
/// publisher using the provided closure.
///
/// - Parameter transform:
/// A closure that takes one element as its parameter
/// and returns a new element.
/// - Returns:
/// A publisher that uses the provided closure to map elements from the
/// upstream publisher to new elements that it then publishes.
public func map<T>(_ transform: @escaping (Output) -> T) -> some CurrentValuePublisher<T, Failure> {
ReadOnlyCurrentValuePublisher(self, transform)
}
/// Returns a publisher that publishes the value of a key path, and the
/// current value at that key path.
///
/// - Parameter keyPath: The key path of a property on `Output`
/// - Returns: A publisher that publishes the value of the key path.
public func map<T>(_ keyPath: KeyPath<Output, T>) -> some CurrentValuePublisher<T, Failure> {
ReadOnlyCurrentValuePublisher(self, keyPath: keyPath)
}
}
// MARK: Internal
extension Publisher {
/// Subscribes and synchronously returns the first value output from this
/// publisher.
///
/// - Warning: Must only be called on a `CurrentValuePublisher`, otherwise
/// this will unconditionally trap.
@usableFromInline
internal func _getValue() -> Output {
var value: Output!
_ = first().sink(
receiveCompletion: { _ in },
receiveValue: { value = $0 }
)
return value
}
}
// MARK: Private
/// A publisher that wraps an upstream `CurrentValuePublisher`, transforming
/// its current value and all values published by it.
private final class ReadOnlyCurrentValuePublisher<Value, Failure: Error>: CurrentValuePublisher {
typealias Output = Value
let _publisher: AnyPublisher<Value, Failure>
let _value: () -> Value
convenience init<Root: CurrentValuePublisher>(
_ subject: Root,
_ transform: @escaping (Root.Output) -> Value
) where Root.Failure == Failure {
self.init(
unsafeSubject: subject,
value: { transform(subject.value) },
transform: transform
)
}
convenience init<Root: CurrentValuePublisher>(
_ subject: Root,
keyPath: KeyPath<Root.Output, Value>
) where Root.Failure == Failure {
self.init(
unsafeSubject: subject,
value: { subject.value[keyPath: keyPath] },
transform: { $0[keyPath: keyPath] }
)
}
init<P: Publisher>(
unsafeSubject subject: P,
value: @escaping () -> Value,
transform: @escaping (P.Output) -> Value
) where P.Failure == Failure {
self._value = value
self._publisher = subject.map(transform).eraseToAnyPublisher()
}
var value: Value {
_value()
}
func receive<S: Subscriber>(subscriber: S) where S.Failure == Failure, S.Input == Value {
_publisher.receive(subscriber: subscriber)
}
}
@Frizlab
Copy link

Frizlab commented Apr 25, 2022

This is exactly what I was searching for and was in the process of writing.
Thank you very much!

@sharplet
Copy link
Author

Update 2025-07-31: Consolidated into a single file and made ReadOnlyCurrentValuePublisher private by adopting opaque return types.

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