Introduce KeyedCursorProtocol, remove ViewStore in favor of forward
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 taggedsendfunctions. This solves one part of what ViewStore was solving. - Introduces
Binding(state:send:tag:)which gives us the binding equivalent toAddress.forward - Introduces
KeyedCursorProtocolwhich 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.
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