diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 83a98159b7aa..7cf2ad00fedb 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -358,6 +358,7 @@ extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewSt /// bindable in SwiftUI views. /// /// Read for more information. +@dynamicMemberLookup @propertyWrapper public struct BindingViewState { let binding: Binding @@ -376,6 +377,12 @@ public struct BindingViewState { public var projectedValue: Binding { self.binding } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> BindingViewState { + BindingViewState(binding: self.binding[dynamicMember: keyPath]) + } } extension BindingViewState: Equatable where Value: Equatable { diff --git a/Tests/ComposableArchitectureTests/Reducers/BindingReducerTests.swift b/Tests/ComposableArchitectureTests/Reducers/BindingReducerTests.swift index 157e9c2e8999..a4e2a04ead54 100644 --- a/Tests/ComposableArchitectureTests/Reducers/BindingReducerTests.swift +++ b/Tests/ComposableArchitectureTests/Reducers/BindingReducerTests.swift @@ -84,6 +84,20 @@ final class BindingTests: BaseTCATestCase { XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!"))) } + func testNestedBindingViewState() { + struct ViewState: Equatable { + @BindingViewState var field: String + } + + let store = Store(initialState: BindingTest.State()) { BindingTest() } + + let viewStore = ViewStore(store, observe: { ViewState(field: $0.$nested.field) }) + + viewStore.$field.wrappedValue = "Hello" + + XCTAssertEqual(store.withState { $0.nested.field }, "Hello!") + } + func testBindingActionUpdatesRespectsPatternMatching() async { let testStore = TestStore( initialState: BindingTest.State(nested: BindingTest.State.Nested(field: ""))