Skip to content

Commit 37b8a41

Browse files
authored
Allow chaining into BindingViewState (#2334)
Currently, `BindingViewStore`s can only directly derive `BindingViewState` for a view state struct, and then the view store can derive a binding and use dynamic member lookup to pluck out a field for a view. This means potentially exposing view state to far more state than necessary. To prevent this we can add dynamic member lookup to the binding view state itself, which allows a view state struct to chip away any state it doesn't care about.
1 parent 5b0ad4c commit 37b8a41

File tree

2 files changed

+21
-0
lines changed

2 files changed

+21
-0
lines changed

Sources/ComposableArchitecture/SwiftUI/Binding.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewSt
358358
/// bindable in SwiftUI views.
359359
///
360360
/// Read <doc:Bindings> for more information.
361+
@dynamicMemberLookup
361362
@propertyWrapper
362363
public struct BindingViewState<Value> {
363364
let binding: Binding<Value>
@@ -376,6 +377,12 @@ public struct BindingViewState<Value> {
376377
public var projectedValue: Binding<Value> {
377378
self.binding
378379
}
380+
381+
public subscript<Subject>(
382+
dynamicMember keyPath: WritableKeyPath<Value, Subject>
383+
) -> BindingViewState<Subject> {
384+
BindingViewState<Subject>(binding: self.binding[dynamicMember: keyPath])
385+
}
379386
}
380387

381388
extension BindingViewState: Equatable where Value: Equatable {

Tests/ComposableArchitectureTests/Reducers/BindingReducerTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ final class BindingTests: BaseTCATestCase {
8484
XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!")))
8585
}
8686

87+
func testNestedBindingViewState() {
88+
struct ViewState: Equatable {
89+
@BindingViewState var field: String
90+
}
91+
92+
let store = Store(initialState: BindingTest.State()) { BindingTest() }
93+
94+
let viewStore = ViewStore(store, observe: { ViewState(field: $0.$nested.field) })
95+
96+
viewStore.$field.wrappedValue = "Hello"
97+
98+
XCTAssertEqual(store.withState { $0.nested.field }, "Hello!")
99+
}
100+
87101
func testBindingActionUpdatesRespectsPatternMatching() async {
88102
let testStore = TestStore(
89103
initialState: BindingTest.State(nested: BindingTest.State.Nested(field: ""))

0 commit comments

Comments
 (0)