diff --git a/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift b/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift index cec9e1672ef5..aa6cc0c2c767 100644 --- a/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift +++ b/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift @@ -1,20 +1,27 @@ #if compiler(>=6) - public typealias _AnyKeyPath = any AnyKeyPath & Sendable - public typealias _PartialKeyPath = any PartialKeyPath & Sendable - public typealias _KeyPath = any KeyPath & Sendable - public typealias _WritableKeyPath = any WritableKeyPath & Sendable - public typealias _ReferenceWritableKeyPath = any ReferenceWritableKeyPath< + public typealias _SendableAnyKeyPath = any AnyKeyPath & Sendable + public typealias _SendablePartialKeyPath = any PartialKeyPath & Sendable + public typealias _SendableKeyPath = any KeyPath & Sendable + public typealias _SendableWritableKeyPath = any WritableKeyPath & Sendable + public typealias _SendableReferenceWritableKeyPath = any ReferenceWritableKeyPath< Root, Value > & Sendable - public typealias _PartialCaseKeyPath = any PartialCaseKeyPath & Sendable - public typealias _CaseKeyPath = any CaseKeyPath & Sendable + public typealias _SendablePartialCaseKeyPath = any PartialCaseKeyPath & Sendable + public typealias _SendableCaseKeyPath = any CaseKeyPath & Sendable #else - public typealias _AnyKeyPath = AnyKeyPath - public typealias _PartialKeyPath = PartialKeyPath - public typealias _KeyPath = KeyPath - public typealias _WritableKeyPath = WritableKeyPath - public typealias _ReferenceWritableKeyPath = ReferenceWritableKeyPath - public typealias _PartialCaseKeyPath = PartialCaseKeyPath - public typealias _CaseKeyPath = CaseKeyPath + public typealias _SendableAnyKeyPath = AnyKeyPath + public typealias _SendablePartialKeyPath = PartialKeyPath + public typealias _SendableKeyPath = KeyPath + public typealias _SendableWritableKeyPath = WritableKeyPath + public typealias _SendableReferenceWritableKeyPath = ReferenceWritableKeyPath + public typealias _SendablePartialCaseKeyPath = PartialCaseKeyPath + public typealias _SendableCaseKeyPath = CaseKeyPath #endif + +@_transparent +func sendableKeyPath( + _ keyPath: AnyKeyPath +) -> _SendableAnyKeyPath { + unsafeBitCast(keyPath, to: _SendableAnyKeyPath.self) +} diff --git a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift index 7f5fcdb4e8ea..3df621fce49c 100644 --- a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift @@ -58,7 +58,7 @@ extension UIBindable { extension BindingAction { public static func set( - _ keyPath: _WritableKeyPath, + _ keyPath: _SendableWritableKeyPath, _ value: Value ) -> Self where Root: ObservableState { .init( @@ -116,7 +116,7 @@ extension BindingAction { extension BindableAction where State: ObservableState { fileprivate static func set( - _ keyPath: _WritableKeyPath, + _ keyPath: _SendableWritableKeyPath, _ value: Value, isInvalidated: (@MainActor @Sendable () -> Bool)? ) -> Self { @@ -150,7 +150,7 @@ extension BindableAction where State: ObservableState { } public static func set( - _ keyPath: _WritableKeyPath, + _ keyPath: _SendableWritableKeyPath, _ value: Value ) -> Self { self.set(keyPath, value, isInvalidated: nil) @@ -159,7 +159,7 @@ extension BindableAction where State: ObservableState { extension Store where State: ObservableState, Action: BindableAction, Action.State == State { public subscript( - dynamicMember keyPath: _WritableKeyPath + dynamicMember keyPath: _SendableWritableKeyPath ) -> Value { get { self.state[keyPath: keyPath] } set { @@ -195,7 +195,7 @@ where Action.ViewAction.State == State { public subscript( - dynamicMember keyPath: _WritableKeyPath + dynamicMember keyPath: _SendableWritableKeyPath ) -> Value { get { self.state[keyPath: keyPath] } set { diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift index 5c41179b0778..c49a13237f9b 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift @@ -293,7 +293,7 @@ extension PresentationAction: CasePathable { } public subscript( - dynamicMember keyPath: _CaseKeyPath + dynamicMember keyPath: _SendableCaseKeyPath ) -> AnyCasePath where Action: CasePathable { AnyCasePath( @@ -307,7 +307,7 @@ extension PresentationAction: CasePathable { @_disfavoredOverload public subscript( - dynamicMember keyPath: _CaseKeyPath + dynamicMember keyPath: _SendableCaseKeyPath ) -> AnyCasePath> where Action: CasePathable { AnyCasePath>( diff --git a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift b/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift index 39bf78864f29..5a14e0aefbc9 100644 --- a/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift +++ b/Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKeyPathKey.swift @@ -15,7 +15,7 @@ extension PersistenceReaderKey { /// - Parameter key: A string key identifying a value to share in memory. /// - Returns: A persistence key. public static func appStorage( - _ keyPath: _ReferenceWritableKeyPath + _ keyPath: _SendableReferenceWritableKeyPath ) -> Self where Self == AppStorageKeyPathKey { AppStorageKeyPathKey(keyPath) } @@ -25,10 +25,10 @@ extension PersistenceReaderKey { /// /// See ``PersistenceReaderKey/appStorage(_:)-5jsie`` to create values of this type. public struct AppStorageKeyPathKey: Sendable { - private let keyPath: _ReferenceWritableKeyPath + private let keyPath: _SendableReferenceWritableKeyPath private let store: UncheckedSendable - public init(_ keyPath: _ReferenceWritableKeyPath) { + public init(_ keyPath: _SendableReferenceWritableKeyPath) { @Dependency(\.defaultAppStorage) var store self.keyPath = keyPath self.store = UncheckedSendable(store) diff --git a/Sources/ComposableArchitecture/SharedState/Shared.swift b/Sources/ComposableArchitecture/SharedState/Shared.swift index 83b8ad5ee10f..5af933b2082b 100644 --- a/Sources/ComposableArchitecture/SharedState/Shared.swift +++ b/Sources/ComposableArchitecture/SharedState/Shared.swift @@ -14,9 +14,9 @@ import IssueReporting @propertyWrapper public struct Shared: Sendable { private let reference: any Reference - private let keyPath: _AnyKeyPath + private let keyPath: _SendableAnyKeyPath - init(reference: any Reference, keyPath: _AnyKeyPath) { + init(reference: any Reference, keyPath: _SendableAnyKeyPath) { self.reference = reference self.keyPath = keyPath } @@ -64,10 +64,9 @@ public struct Shared: Sendable { reference: base.reference, // NB: Can get rid of bitcast when this is fixed: // https://github.com/swiftlang/swift/issues/75531 - keyPath: unsafeBitCast( + keyPath: sendableKeyPath( (base.keyPath as AnyKeyPath) - .appending(path: \Value?.[default: DefaultSubscript(initialValue)])!, - to: _AnyKeyPath.self + .appending(path: \Value?.[default: DefaultSubscript(initialValue)])! ) ) } @@ -180,9 +179,8 @@ public struct Shared: Sendable { reference: self.reference, // NB: Can get rid of bitcast when this is fixed: // https://github.com/swiftlang/swift/issues/75531 - keyPath: unsafeBitCast( - (self.keyPath as AnyKeyPath).appending(path: keyPath)!, - to: _AnyKeyPath.self + keyPath: sendableKeyPath( + (self.keyPath as AnyKeyPath).appending(path: keyPath)! ) ) } @@ -463,9 +461,8 @@ extension Shared { reference: self.reference, // NB: Can get rid of bitcast when this is fixed: // https://github.com/swiftlang/swift/issues/75531 - keyPath: unsafeBitCast( - (self.keyPath as AnyKeyPath).appending(path: keyPath)!, - to: _AnyKeyPath.self + keyPath: sendableKeyPath( + (self.keyPath as AnyKeyPath).appending(path: keyPath)! ) ) } diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 5d19a605edc2..23613536c148 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -144,7 +144,7 @@ extension BindingState: Sendable where Value: Sendable {} /// /// Read for more information. public struct BindingAction: CasePathable, Equatable, Sendable { - public let keyPath: _PartialKeyPath + public let keyPath: _SendablePartialKeyPath @usableFromInline let set: @Sendable (inout Root) -> Void @@ -152,7 +152,7 @@ public struct BindingAction: CasePathable, Equatable, Sendable { let valueIsEqualTo: @Sendable (Any) -> Bool init( - keyPath: _PartialKeyPath, + keyPath: _SendablePartialKeyPath, set: @escaping @Sendable (inout Root) -> Void, value: any Sendable, valueIsEqualTo: @escaping @Sendable (Any) -> Bool @@ -174,7 +174,7 @@ public struct BindingAction: CasePathable, Equatable, Sendable { @dynamicMemberLookup public struct AllCasePaths { public subscript( - dynamicMember keyPath: _WritableKeyPath + dynamicMember keyPath: _SendableWritableKeyPath ) -> AnyCasePath where Root: ObservableState { AnyCasePath( embed: { .set(keyPath, $0) }, @@ -183,7 +183,7 @@ public struct BindingAction: CasePathable, Equatable, Sendable { } public subscript( - dynamicMember keyPath: _WritableKeyPath> + dynamicMember keyPath: _SendableWritableKeyPath> ) -> AnyCasePath { AnyCasePath( embed: { .set(keyPath, $0) }, @@ -204,7 +204,7 @@ extension BindingAction { /// - Returns: An action that describes simple mutations to some root state at a writable key /// path. public static func set( - _ keyPath: _WritableKeyPath>, + _ keyPath: _SendableWritableKeyPath>, _ value: Value ) -> Self { return .init( @@ -234,7 +234,7 @@ extension BindingAction { } init( - keyPath: _WritableKeyPath>, + keyPath: _SendableWritableKeyPath>, set: @escaping @Sendable (_ state: inout Root) -> Void, value: Value ) { @@ -290,7 +290,7 @@ extension BindableAction { /// /// - Returns: A binding action. public static func set( - _ keyPath: _WritableKeyPath>, + _ keyPath: _SendableWritableKeyPath>, _ value: Value ) -> Self { self.binding(.set(keyPath, value)) @@ -299,7 +299,7 @@ extension BindableAction { extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewState { public subscript( - dynamicMember keyPath: _WritableKeyPath> + dynamicMember keyPath: _SendableWritableKeyPath> ) -> Binding { self.binding( get: { $0[keyPath: keyPath].wrappedValue }, @@ -454,7 +454,7 @@ public struct BindingViewStore { } public subscript( - dynamicMember keyPath: _WritableKeyPath> + dynamicMember keyPath: _SendableWritableKeyPath> ) -> BindingViewState { BindingViewState( binding: ViewStore(self.store, observe: { $0[keyPath: keyPath].wrappedValue }) diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index caa68f5c1e43..5864827c3525 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -1918,7 +1918,7 @@ extension TestStore where State: Equatable { @_disfavoredOverload @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) public func receive( - _ actionCase: _CaseKeyPath, + _ actionCase: _SendableCaseKeyPath, _ value: Value, timeout duration: Duration, assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil, diff --git a/Tests/ComposableArchitectureTests/AppStorageTests.swift b/Tests/ComposableArchitectureTests/AppStorageTests.swift index 044ad9814ab7..0cd414a91e22 100644 --- a/Tests/ComposableArchitectureTests/AppStorageTests.swift +++ b/Tests/ComposableArchitectureTests/AppStorageTests.swift @@ -285,7 +285,7 @@ final class AppStorageTests: XCTestCase { await fulfillment(of: [perceptionExpectation, publisherExpectation], timeout: 1) } - func testUpdateStoreFromBackgroundThread_KeyPath() async throws { + func testUpdateStoreFromBackgroundThread_SendableKeyPath() async throws { @Dependency(\.defaultAppStorage) var store @Shared(.appStorage(\.count)) var count = 0 diff --git a/Tests/ComposableArchitectureTests/EffectTests.swift b/Tests/ComposableArchitectureTests/EffectTests.swift index d821a5bb352b..bcee7ad81364 100644 --- a/Tests/ComposableArchitectureTests/EffectTests.swift +++ b/Tests/ComposableArchitectureTests/EffectTests.swift @@ -50,6 +50,7 @@ final class EffectTests: BaseTCATestCase { await withMainSerialExecutor { let values = LockIsolated<[Int]>([]) + let effect = Effect.concatenate( .publisher { Just(1).delay(for: 1, scheduler: self.mainQueue) } )