-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathConnectedContainer.swift
More file actions
217 lines (198 loc) · 10.8 KB
/
ConnectedContainer.swift
File metadata and controls
217 lines (198 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//===--- ConnectedContainer.swift ------------------------------------------===//
//
// This source file is part of the UDF open source project
//
// Copyright (c) 2024 You are launched
// Licensed under Apache License v2.0
//
// See https://opensource.org/licenses/Apache-2.0 for license information
//
//===----------------------------------------------------------------------===//
import Foundation
import SwiftUI
import Runtime
/// A SwiftUI view that connects a `Component` with its associated state in the UDF architecture.
///
/// The `ConnectedContainer` is responsible for managing the lifecycle and state of a given component.
/// It observes changes in the global state (`EnvironmentStore`), applies hooks, and manages view
/// appearance and disappearance. This view serves as the bridge between the app's state and the UI,
/// ensuring that the state updates are reflected in the view.
///
/// ## Generic Parameters:
/// - `C`: A type conforming to `Component` that defines the UI.
/// - `State`: A type conforming to `AppReducer` representing the global state.
///
/// ## Properties:
/// - `map`: A closure that maps the global store to the properties needed by the component.
/// - `scope`: A closure that extracts a specific scope from the global state.
/// - `onContainerAppear`: A closure executed when the container appears in the view hierarchy.
/// - `onContainerDisappear`: A closure executed when the container disappears from the view hierarchy.
/// - `containerLifecycle`: A `ContainerLifecycle` instance that manages the container's lifecycle events.
/// - `containerState`: A `ContainerState` instance that observes changes in the scoped state.
///
/// ## Initialization:
/// - `init(map:scope:onContainerAppear:onContainerDisappear:onContainerDidLoad:onContainerDidUnload:useHooks:)`:
/// Initializes the `ConnectedContainer` with closures to manage state mapping, scope, lifecycle events, and hooks.
/// - `init<BindedContainer: BindableContainer>(...)`: Initializes a `ConnectedContainer` for a bindable container type, managing state and
/// lifecycle events.
///
/// ## Methods:
/// - `body`: The main view builder, responsible for creating the component and attaching lifecycle events.
///
/// ## Usage:
/// The `ConnectedContainer` can be used to encapsulate a component and automatically react to state changes,
/// manage its lifecycle events, and bind hooks for actions and side effects.
struct ConnectedContainer<C: Component, State: AppReducer>: View {
/// A closure that maps the global store to the properties needed by the component.
let map: (_ store: EnvironmentStore<State>) -> C.Props
/// A closure that defines the scope within the global state.
let scope: @MainActor (_ state: State) -> Scope
/// A closure executed when the container appears in the view hierarchy.
var onContainerAppear: @MainActor (EnvironmentStore<State>) -> Void
/// A closure executed when the container disappears from the view hierarchy.
var onContainerDisappear: @MainActor (EnvironmentStore<State>) -> Void
/// The container's lifecycle manager that handles loading, unloading, and hooks.
@StateObject var containerLifecycle: ContainerLifecycle<State>
/// The container state that observes changes in the scoped state.
@ObservedObject var containerState: ContainerState<State>
/// Provides access to the `EnvironmentStore`, which can be either global or explicitly provided.
private var store: EnvironmentStore<State>
/// Initializes the `ConnectedContainer` with an store and closures for mapping state, managing scope,
/// handling lifecycle events, and creating hooks.
///
/// - Parameters:
/// - store: The `EnvironmentStore` instance to use for this container. Defaults to `.global` if not provided.
/// - map: A closure to map the `EnvironmentStore` to the component's properties.
/// - scope: A closure to extract a specific scope from the global state.
/// - onContainerAppear: A closure executed when the container appears.
/// - onContainerDisappear: A closure executed when the container disappears.
/// - onContainerDidLoad: A closure executed when the container is loaded.
/// - onContainerDidUnload: A closure executed when the container is unloaded.
/// - useHooks: A closure that provides an array of hooks to use within the container.
init(
store: EnvironmentStore<State>,
map: @escaping (EnvironmentStore<State>) -> C.Props,
scope: @escaping @Sendable (State) -> Scope,
onContainerAppear: @escaping @MainActor (EnvironmentStore<State>) -> Void,
onContainerDisappear: @escaping @MainActor (EnvironmentStore<State>) -> Void,
onContainerDidLoad: @escaping (EnvironmentStore<State>) -> Void,
onContainerDidUnload: @escaping (EnvironmentStore<State>) -> Void,
useHooks: @escaping () -> [Hook<State>]
) {
self.store = store
self.map = map
self.scope = scope
self.onContainerAppear = onContainerAppear
self.onContainerDisappear = onContainerDisappear
self._containerLifecycle = .init(
wrappedValue: ContainerLifecycle(
didLoadCommand: onContainerDidLoad,
didUnloadCommand: onContainerDidUnload,
useHooks: useHooks
)
)
self._containerState = .init(wrappedValue: .init(store: store, scope: scope))
}
/// Initializes a `ConnectedContainer` for a bindable container type, managing state and lifecycle events.
///
/// - Parameters:
/// - store: The `EnvironmentStore` instance to use for this container. Defaults to `.global` if not provided.
/// - containerType: The type of the bindable container.
/// - containerId: A closure that returns the container's unique identifier.
/// - map: A closure to map the `EnvironmentStore` to the component's properties.
/// - scope: A closure to extract a specific scope from the global state.
/// - onContainerAppear: A closure executed when the container appears.
/// - onContainerDisappear: A closure executed when the container disappears.
/// - onContainerDidLoad: A closure executed when the container is loaded.
/// - onContainerDidUnload: A closure executed when the container is unloaded.
/// - onBindableContainerStateDidLoad: A closure executed when the bindable container's state is loaded and verified online.
/// - onBindableContainerStateDidUnload: A closure executed when the bindable container's state is unloaded and verified offline.
/// - useHooks: A closure that provides an array of hooks to use within the container.
init<BindedContainer: BindableContainer>(
store: EnvironmentStore<State>,
containerType: BindedContainer.Type,
containerId: @escaping () -> BindedContainer.ID,
map: @escaping (EnvironmentStore<State>) -> C.Props,
scope: @escaping @Sendable (State) -> Scope,
onContainerAppear: @escaping @MainActor (EnvironmentStore<State>) -> Void,
onContainerDisappear: @escaping @MainActor (EnvironmentStore<State>) -> Void,
onContainerDidLoad: @escaping (EnvironmentStore<State>) -> Void,
onContainerDidUnload: @escaping (EnvironmentStore<State>) -> Void,
onBindableContainerStateDidLoad: @escaping (EnvironmentStore<State>) -> Void,
onBindableContainerStateDidUnload: @escaping (EnvironmentStore<State>) -> Void,
useHooks: @escaping () -> [Hook<State>]
) where BindedContainer.ID: Sendable {
self.store = store
self.map = map
self.scope = scope
self.onContainerAppear = onContainerAppear
self.onContainerDisappear = onContainerDisappear
self._containerLifecycle = .init(
wrappedValue: ContainerLifecycle(
didLoadCommand: { store in
store.dispatch(
Actions._OnContainerDidLoad(containerType: containerType, id: containerId()).silent(),
priority: .userInteractive
)
onContainerDidLoad(store)
let boundReducer = Self.getBoundReducer(with: store, for: containerType)
if boundReducer?.hasReducer(for: containerId()) == false {
onBindableContainerStateDidLoad(store)
}
},
didUnloadCommand: { store in
onContainerDidUnload(store)
let boundReducer = Self.getBoundReducer(with: store, for: containerType)
if boundReducer?.isLastInstance(for: containerId()) == true {
onBindableContainerStateDidUnload(store)
}
store.dispatch(
Actions._OnContainerDidUnLoad(containerType: containerType, id: containerId())
.with(delay: 0.15)
.silent()
)
},
useHooks: useHooks
)
)
self._containerState = .init(wrappedValue: .init(store: store, scope: scope))
}
/// The main view body that renders the component and attaches lifecycle events.
var body: some View {
containerLifecycle.set(didLoad: true, store: store)
return C(props: map(store))
.onAppear { onContainerAppear(store) }
.onDisappear { onContainerDisappear(store) }
}
}
extension ConnectedContainer {
/// Resolves and returns the bound reducer associated with a container type.
///
/// Uses cached property metadata inside the `SourceOfTruth` wrapper to avoid repeatedly reflecting over
/// the entire `AppState` properties list.
///
/// - Parameters:
/// - store: The environment store containing the state and cache.
/// - type: The type of the bindable container.
/// - Returns: The matched bindable reducer existential, or `nil` if not found.
static func getBoundReducer<T: BindableContainer>(
with store: EnvironmentStore<State>,
for type: T.Type
) -> (any AnyBindableReducer)? {
if let property = store.$state.getPropertyMetadata(for: T.self) {
return try? property.get(from: store.state) as? AnyBindableReducer
}
guard let info = try? typeInfo(of: State.self) else {
return nil
}
for property in info.properties {
if let bindableReducer = try? property.get(from: store.state) as? AnyBindableReducer {
store.$state.setPropertyMetadata(property, for: bindableReducer.boundContainerType)
if bindableReducer.boundContainerType == T.self {
return bindableReducer
}
}
}
return nil
}
}