diff --git a/Sources/Sharing/Internal/Reference.swift b/Sources/Sharing/Internal/Reference.swift index f5d98ca..7ad71cc 100644 --- a/Sources/Sharing/Internal/Reference.swift +++ b/Sources/Sharing/Internal/Reference.swift @@ -613,6 +613,56 @@ where Base: MutableReference, Path: WritableKeyPath { } } +final class _ReadClosureReference: + Reference, + Observable +{ + private let base: Base + private let body: @Sendable (Base.Value) -> Value + + init(base: Base, body: @escaping @Sendable (Base.Value) -> Value) { + self.base = base + self.body = body + } + + var id: ObjectIdentifier { + base.id + } + + var isLoading: Bool { + base.isLoading + } + + var loadError: (any Error)? { + base.loadError + } + + var wrappedValue: Value { + body(base.wrappedValue) + } + + func load() async throws { + try await base.load() + } + + func touch() { + base.touch() + } + + #if canImport(Combine) + var publisher: any Publisher { + func open(_ publisher: some Publisher) -> any Publisher { + publisher.map(body) + } + return open(base.publisher) + } + #endif + + var description: String { + ".map(\(base.description), as: \(Value.self).self)" + } +} + final class _OptionalReference, Value>: Reference, Observable, diff --git a/Sources/Sharing/Shared.swift b/Sources/Sharing/Shared.swift index 43f591c..fd75f57 100644 --- a/Sources/Sharing/Shared.swift +++ b/Sources/Sharing/Shared.swift @@ -223,6 +223,15 @@ public struct Shared { reference = newValue.reference } } + + /// Returns a read-only shared reference to the resulting value of a given closure. + /// + /// - Returns: A new read-only shared reference. + public func read( + _ body: @escaping @Sendable(Value) -> Member + ) -> SharedReader { + SharedReader(self).read(body) + } /// Returns a shared reference to the resulting value of a given key path. /// diff --git a/Sources/Sharing/SharedReader.swift b/Sources/Sharing/SharedReader.swift index f2bec1b..6615219 100644 --- a/Sources/Sharing/SharedReader.swift +++ b/Sources/Sharing/SharedReader.swift @@ -155,6 +155,20 @@ public struct SharedReader { reference = newValue.reference } } + + /// Returns a read-only shared reference to the resulting value of a given closure. + /// + /// - Returns: A new shared reader. + public func read( + _ body: @escaping @Sendable (Value) -> Member + ) -> SharedReader { + func open(_ reference: some Reference) -> SharedReader { + SharedReader( + reference: _ReadClosureReference(base: reference, body: body) + ) + } + return open(reference) + } /// Returns a read-only shared reference to the resulting value of a given key path. /// diff --git a/Tests/SharingTests/EquatableTests.swift b/Tests/SharingTests/EquatableTests.swift index f294d23..2103101 100644 --- a/Tests/SharingTests/EquatableTests.swift +++ b/Tests/SharingTests/EquatableTests.swift @@ -32,4 +32,18 @@ struct EquatableTests { #expect(lhs == rhs) #expect($lhs == $rhs) } + + @Test func mapReader() { + @Shared(value: 0) var base: Int + @SharedReader var lhs: Int + @SharedReader var rhs: Int + _lhs = $base.read { $0 * 2 } + _rhs = $base.read { $0 * 3 } + #expect(lhs == rhs) + #expect($lhs == $rhs) + + $base.withLock { $0 += 1 } + #expect(lhs != rhs) + #expect($lhs != $rhs) + } } diff --git a/Tests/SharingTests/SharedTests.swift b/Tests/SharingTests/SharedTests.swift index 2e81813..c9cbbf7 100644 --- a/Tests/SharingTests/SharedTests.swift +++ b/Tests/SharingTests/SharedTests.swift @@ -87,6 +87,22 @@ import Testing #expect(id == 42) } + + @Test func mapReader() { + @Shared(value: 0) var count + @SharedReader var isZero: Bool + _isZero = $count.read { $0 == 0 } + + #expect(isZero) + + $count.withLock { $0 += 1 } + + #expect(!isZero) + + $count = Shared(value: 0) + + #expect(!isZero) + } @Test func optional() throws { @Shared(value: nil) var wrappedCount: Int?