Creating nested components
Usually, you will need to create new components in terms of others with lower granularity. This page covers the most important aspects you need to consider when composing different components.
Creating a parent view
As components conform to View
, nothing stops you from adding them to your SwiftUI views. Therefore, assuming you already have a ChildComponent
, you can easily add it to your view hierarchy:
struct ParentView: View {
let state: ParentState
let child: ChildComponent
var body: some View {
VStack {
// ... Some views ...
child
// ... Some views ...
}
}
}
However, this forces to build the ChildComponent
if, for instance, we would like to create a preview of ParentView
. An alternative way of doing this is by parameterizing the ParentView
:
struct ParentView<Child: View>: View {
let state: ParentState
let child: Child
var body: some View {
VStack {
// ... Some views ...
child
// ... Some views ...
}
}
}
With this small change, we can now pass the ChildComponent, or any other stub view that we want in order to render the preview.
One additional step we can take is to pass a function that, given the child state, builds the corresponding component. This is particularly useful when we are dealing with collections of items.
struct ParentView<Child: View>: View {
let state: ParentState
let child: (ChildState) -> Child
let handle: (ParentInput) -> Void
var body: some View {
VStack {
// ... Some views ...
child(self.state.childState)
// ... Some views ...
}
}
}
Creating a global dispatcher
Next, both child and parent will have their own Dispatchers to interpret view inputs into state mutations. Those Dispatchers need to be combined into a single one. However, types of both dispatchers do not match:
typealias ChildDispatcher = StateDispatcher<ChildDependencies, ChildState, ChildInput>
typealias ParentDispatcher = StateDispatcher<ParentDependencies, ParentState, ParentInput>
In order to combine them, first we need to match their type signatures. As the ChildDependencies
, ChildState
and ChildInput
are embedded into ParentDependencies
, ParentState
and ParentInput
respectively, we can widen
the ChildDispatcher
to embed each parameter into its corresponding slot in the parent:
let widenChildDispatcher: ParentDispatcher =
childDispatcher.widen(
transformEnvironment: \.childDependencies,
transformState: ParentState.childLens,
transformInput: ParentInput.prism(for: ParentInput.childInput))
let combinedDispatcher = parentDispatcher.combine(widenChildDispatcher)
Assembling the parent component
Finally, when you create the parent component, you will need to forward the inputs that happen on the child component to the parent component, as some of these inputs may be relevant to other components that are upstream in the view hierarchy.
func parentComponent(
initialState: ParentState,
dependencies: ParentDependencies
) -> ParentComponent {
ParentComponent(
initialState: initialState,
environment: dependencies,
dispatcher: combinedDispatcher,
render: { state, handle in
ParentView(
state: state,
child: { childState in
childComponent(childState)
.using(handle,
transformInput: ParentInput.prism(for: ParentInput.childInput))
},
handle: handle)
}
)
}