diff --git a/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift b/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift index fae979cff8d1..65c52c18de63 100644 --- a/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift +++ b/Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift @@ -22,13 +22,69 @@ public typealias _SendableCaseKeyPath = CaseKeyPath #endif -@_transparent -func sendableKeyPath( - _ keyPath: AnyKeyPath -) -> _SendableAnyKeyPath { - #if compiler(>=6) - unsafeBitCast(keyPath, to: _SendableAnyKeyPath.self) - #else - keyPath - #endif +// NB: Dynamic member lookup does not currently support sendable key paths and even breaks +// autocomplete. +// +// * https://github.com/swiftlang/swift/issues/77035 +// * https://github.com/swiftlang/swift/issues/77105 +extension _AppendKeyPath { + @_transparent + func unsafeSendable() -> _SendableAnyKeyPath + where Self == AnyKeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendableAnyKeyPath.self) + #else + self + #endif + } + + @_transparent + func unsafeSendable() -> _SendablePartialKeyPath + where Self == PartialKeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendablePartialKeyPath.self) + #else + self + #endif + } + + @_transparent + func unsafeSendable() -> _SendableKeyPath + where Self == KeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendableKeyPath.self) + #else + self + #endif + } + + @_transparent + func unsafeSendable() -> _SendableWritableKeyPath + where Self == WritableKeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendableWritableKeyPath.self) + #else + self + #endif + } + + @_transparent + func unsafeSendable() -> _SendableReferenceWritableKeyPath + where Self == ReferenceWritableKeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendableReferenceWritableKeyPath.self) + #else + self + #endif + } + + @_transparent + func unsafeSendable() -> _SendableCaseKeyPath + where Self == CaseKeyPath { + #if compiler(>=6) + unsafeBitCast(self, to: _SendableCaseKeyPath.self) + #else + self + #endif + } } diff --git a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift index 3df621fce49c..03d504769d22 100644 --- a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift @@ -159,12 +159,12 @@ extension BindableAction where State: ObservableState { extension Store where State: ObservableState, Action: BindableAction, Action.State == State { public subscript( - dynamicMember keyPath: _SendableWritableKeyPath + dynamicMember keyPath: WritableKeyPath ) -> Value { get { self.state[keyPath: keyPath] } set { BindingLocal.$isActive.withValue(true) { - self.send(.set(keyPath, newValue, isInvalidated: _isInvalidated)) + self.send(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated)) } } } @@ -195,12 +195,12 @@ where Action.ViewAction.State == State { public subscript( - dynamicMember keyPath: _SendableWritableKeyPath + dynamicMember keyPath: WritableKeyPath ) -> Value { get { self.state[keyPath: keyPath] } set { BindingLocal.$isActive.withValue(true) { - self.send(.view(.set(keyPath, newValue, isInvalidated: _isInvalidated))) + self.send(.view(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated))) } } } diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift index c49a13237f9b..583090ffa683 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift @@ -293,10 +293,11 @@ extension PresentationAction: CasePathable { } public subscript( - dynamicMember keyPath: _SendableCaseKeyPath + dynamicMember keyPath: CaseKeyPath ) -> AnyCasePath where Action: CasePathable { - AnyCasePath( + let keyPath = keyPath.unsafeSendable() + return AnyCasePath( embed: { .presented(keyPath($0)) }, extract: { guard case let .presented(action) = $0 else { return nil } @@ -307,10 +308,11 @@ extension PresentationAction: CasePathable { @_disfavoredOverload public subscript( - dynamicMember keyPath: _SendableCaseKeyPath + dynamicMember keyPath: CaseKeyPath ) -> AnyCasePath> where Action: CasePathable { - AnyCasePath>( + let keyPath = keyPath.unsafeSendable() + return AnyCasePath>( embed: { switch $0 { case .dismiss: diff --git a/Sources/ComposableArchitecture/SharedState/Shared.swift b/Sources/ComposableArchitecture/SharedState/Shared.swift index 5af933b2082b..170c705b9df2 100644 --- a/Sources/ComposableArchitecture/SharedState/Shared.swift +++ b/Sources/ComposableArchitecture/SharedState/Shared.swift @@ -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: sendableKeyPath( - (base.keyPath as AnyKeyPath) - .appending(path: \Value?.[default: DefaultSubscript(initialValue)])! - ) + keyPath: (base.keyPath as AnyKeyPath) + .appending(path: \Value?.[default: DefaultSubscript(initialValue)])! + .unsafeSendable() ) } @@ -179,9 +178,9 @@ 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: sendableKeyPath( - (self.keyPath as AnyKeyPath).appending(path: keyPath)! - ) + keyPath: (self.keyPath as AnyKeyPath) + .appending(path: keyPath)! + .unsafeSendable() ) } @@ -459,11 +458,7 @@ extension Shared { ) -> SharedReader { SharedReader( reference: self.reference, - // NB: Can get rid of bitcast when this is fixed: - // https://github.com/swiftlang/swift/issues/75531 - keyPath: sendableKeyPath( - (self.keyPath as AnyKeyPath).appending(path: keyPath)! - ) + keyPath: (self.keyPath as AnyKeyPath).appending(path: keyPath)!.unsafeSendable() ) } diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 23613536c148..655fdfc42793 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -174,18 +174,20 @@ public struct BindingAction: CasePathable, Equatable, Sendable { @dynamicMemberLookup public struct AllCasePaths { public subscript( - dynamicMember keyPath: _SendableWritableKeyPath + dynamicMember keyPath: WritableKeyPath ) -> AnyCasePath where Root: ObservableState { - AnyCasePath( + let keyPath = keyPath.unsafeSendable() + return AnyCasePath( embed: { .set(keyPath, $0) }, extract: { $0.keyPath == keyPath ? $0.value as? Value : nil } ) } public subscript( - dynamicMember keyPath: _SendableWritableKeyPath> + dynamicMember keyPath: WritableKeyPath> ) -> AnyCasePath { - AnyCasePath( + let keyPath = keyPath.unsafeSendable() + return AnyCasePath( embed: { .set(keyPath, $0) }, extract: { $0.keyPath == keyPath ? $0.value as? Value : nil } ) @@ -299,9 +301,10 @@ extension BindableAction { extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewState { public subscript( - dynamicMember keyPath: _SendableWritableKeyPath> + dynamicMember keyPath: WritableKeyPath> ) -> Binding { - self.binding( + let keyPath = keyPath.unsafeSendable() + return self.binding( get: { $0[keyPath: keyPath].wrappedValue }, send: { value in #if DEBUG @@ -454,9 +457,10 @@ public struct BindingViewStore { } public subscript( - dynamicMember keyPath: _SendableWritableKeyPath> + dynamicMember keyPath: WritableKeyPath> ) -> BindingViewState { - BindingViewState( + let keyPath = keyPath.unsafeSendable() + return BindingViewState( binding: ViewStore(self.store, observe: { $0[keyPath: keyPath].wrappedValue }) .binding( send: { value in