-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce KeyedCursorProtocol, remove ViewStore in favor of forward (#19
) Fixes #18. This PR sketches out one potential solution to #18. It refactors our approach to sub-components by decomplecting action sending from state getting. - Removes `ViewStore` - Introduces `Address.forward(send:tag:)` which gives us an easy way to create tagged `send` functions. This solves one part of what ViewStore was solving. - Introduces `Binding(get:send:tag:)` which gives us the binding equivalent to `Address.forward` - Introduces `KeyedCursorProtocol` which offers an alternative cursor for subcomponents that need to be looked up within dynamic lists. This refactor is in response to the awkwardness of the `ViewStore/Cursor` paradigm for components that are part of a dynamic list. Even if we had created a keyed cursor initializer for ViewStore, it necessarily would have had to hold an optional (nillable) state. This is because ViewStore lookup was dynamic, and this trips up the lifetime typechecking around the model. In practice, a view would not exist if its model did not exist, but this is not a typesafe guarantee for dynamic list lookups. Anyway, the whole paradigm of looking up child from parent dynamically is a bit odd for list items. In SwiftUI the typical approach is to ForEach, and then pass the model data down as a static property to the view. This guarantees type safety, since a view holds its own copy of the data. What if we could do something more like that? The approach in this PR leans into this approach. State can be passed to sub-components as plain old properties. `Address.forward` can be used to create view-local send functions that you can pass down to sub-views. `Binding` gets a similar form. In both cases, we can use a closure to capture additional parent-scoped state, such as an ID for lookup within the parent model. Cursor sticks around, but mostly as a convenient way to create update functions for sub-components. We also introduce `KeyedCursorProtocol` which offers a keyed equivalent for dynamic lookup. ## Usage Sub-components become more "vanilla", just using bare properties and closures. ```swift struct ParentView: View { @StateObject = Store( AppModel(), AppEnvironment() ) var body: some View { ChildModel( state: store.state.child, send: Address.forward( send: store.send, tag: ParentChildCursor.tag ) ) } } struct ChildView: View { var state: ChildModel var send: (ChildAction) -> Void var body: some View { Button(state.text) { send(.activate) } } } ``` ## Prior art This approach is inspired by Reflex: - Forward https://github.com/mozilla/reflex/blob/c5e75e98bc601e2315b6d43e5e347263cf67359e/src/signal.js#L5 - Cursor https://github.com/browserhtml/browserhtml/blob/master/src/Common/Cursor.js
- Loading branch information
1 parent
e54ae1d
commit 332458e
Showing
5 changed files
with
350 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// | ||
// BindingTests.swift | ||
// | ||
// | ||
// Created by Gordon Brander on 9/21/22. | ||
// | ||
|
||
import XCTest | ||
import SwiftUI | ||
@testable import ObservableStore | ||
|
||
final class BindingTests: XCTestCase { | ||
enum Action: Hashable { | ||
case setText(String) | ||
} | ||
|
||
struct Model: ModelProtocol { | ||
var text = "" | ||
var edits: Int = 0 | ||
|
||
static func update( | ||
state: Model, | ||
action: Action, | ||
environment: Void | ||
) -> Update<Model> { | ||
switch action { | ||
case .setText(let text): | ||
var model = state | ||
model.text = text | ||
model.edits = model.edits + 1 | ||
return Update(state: model) | ||
} | ||
} | ||
} | ||
|
||
struct SimpleView: View { | ||
@Binding var text: String | ||
|
||
var body: some View { | ||
Text(text) | ||
} | ||
} | ||
|
||
/// Test creating binding for an address | ||
func testBinding() throws { | ||
let store = Store( | ||
state: Model(), | ||
environment: () | ||
) | ||
|
||
let binding = Binding( | ||
get: { store.state.text }, | ||
send: store.send, | ||
tag: Action.setText | ||
) | ||
|
||
let view = SimpleView(text: binding) | ||
|
||
view.text = "Foo" | ||
view.text = "Bar" | ||
|
||
XCTAssertEqual( | ||
store.state.text, | ||
"Bar" | ||
) | ||
XCTAssertEqual( | ||
store.state.edits, | ||
2 | ||
) | ||
} | ||
} |
Oops, something went wrong.