mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-24 12:14:25 +01:00
Improve performance of ForEachStore (#386)
* Improve performance of ForEachStore * fix * Fix * Cleanup * Basic docs * Update ForEachStore.swift
This commit is contained in:
@@ -1,6 +1,61 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A structure that computes views on demand from a store on a collection of data.
|
||||
/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with
|
||||
/// collections of state.
|
||||
///
|
||||
/// `ForEachStore` loops over a store's collection with a store scoped to the domain of each
|
||||
/// element. This allows you to extract and modularize an element's view and avoid concerns around
|
||||
/// collection index math and parent-child store communication.
|
||||
///
|
||||
/// For example, a todos app may define the domain and logic associated with an individual todo:
|
||||
///
|
||||
/// struct TodoState: Equatable, Identifiable {
|
||||
/// let id: UUID
|
||||
/// var description = ""
|
||||
/// var isComplete = false
|
||||
/// }
|
||||
/// enum TodoAction {
|
||||
/// case isCompleteToggled(Bool)
|
||||
/// case descriptionChanged(String)
|
||||
/// }
|
||||
/// struct TodoEnvironment {}
|
||||
/// let todoReducer = Reducer<TodoState, TodoAction, TodoEnvironment { ... }
|
||||
///
|
||||
/// As well as a view with a domain-specific store:
|
||||
///
|
||||
/// struct TodoView: View {
|
||||
/// let store: Store<TodoState, TodoAction>
|
||||
/// var body: some View { ... }
|
||||
/// }
|
||||
///
|
||||
/// For a parent domain to work with a collection of todos, it can hold onto this collection in
|
||||
/// state:
|
||||
///
|
||||
/// struct AppState: Equatable {
|
||||
/// var todos: IdentifiedArrayOf<TodoState> = []
|
||||
/// }
|
||||
///
|
||||
/// Define a case to handle actions sent to the child domain:
|
||||
///
|
||||
/// enum AppAction {
|
||||
/// case todo(id: TodoState.ID, action: TodoAction)
|
||||
/// }
|
||||
///
|
||||
/// Enhance its reducer using `forEach`:
|
||||
///
|
||||
/// let appReducer = todoReducer.forEach(
|
||||
/// state: \.todos,
|
||||
/// action: /AppAction.todo(id:action:),
|
||||
/// environment: { _ in TodoEnvironment() }
|
||||
/// )
|
||||
///
|
||||
/// And finally render a list of `TodoView`s using `ForEachStore`:
|
||||
///
|
||||
/// ForEachStore(
|
||||
/// self.store.scope(state: \.todos, AppAction.todo(id:action:))
|
||||
/// ) { todoStore in
|
||||
/// TodoView(store: todoStore)
|
||||
/// }
|
||||
public struct ForEachStore<EachState, EachAction, Data, ID, Content>: DynamicViewContent
|
||||
where Data: Collection, ID: Hashable, Content: View {
|
||||
public let data: Data
|
||||
@@ -22,26 +77,17 @@ where Data: Collection, ID: Hashable, Content: View {
|
||||
Data == [EachState],
|
||||
EachContent: View,
|
||||
Content == WithViewStore<
|
||||
Data, (Data.Index, EachAction),
|
||||
ForEach<ContiguousArray<(Data.Index, EachState)>, ID, EachContent>
|
||||
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
|
||||
>
|
||||
{
|
||||
self.data = ViewStore(store, removeDuplicates: { _, _ in false }).state
|
||||
let data = store.state.value
|
||||
self.data = data
|
||||
self.content = {
|
||||
WithViewStore(
|
||||
store,
|
||||
removeDuplicates: { lhs, rhs in
|
||||
guard lhs.count == rhs.count else { return false }
|
||||
return zip(lhs, rhs).allSatisfy { $0[keyPath: id] == $1[keyPath: id] }
|
||||
}
|
||||
) { viewStore in
|
||||
ForEach(
|
||||
ContiguousArray(zip(viewStore.indices, viewStore.state)),
|
||||
id: (\(Data.Index, EachState).1).appending(path: id)
|
||||
) { index, element in
|
||||
WithViewStore(store.scope(state: { $0.map { $0[keyPath: id] } })) { viewStore in
|
||||
ForEach(Array(viewStore.state.enumerated()), id: \.element) { index, _ in
|
||||
content(
|
||||
store.scope(
|
||||
state: { index < $0.endIndex ? $0[index] : element },
|
||||
state: { index < $0.endIndex ? $0[index] : data[index] },
|
||||
action: { (index, $0) }
|
||||
)
|
||||
)
|
||||
@@ -64,8 +110,7 @@ where Data: Collection, ID: Hashable, Content: View {
|
||||
Data == [EachState],
|
||||
EachContent: View,
|
||||
Content == WithViewStore<
|
||||
Data, (Data.Index, EachAction),
|
||||
ForEach<ContiguousArray<(Data.Index, EachState)>, ID, EachContent>
|
||||
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
|
||||
>,
|
||||
EachState: Identifiable,
|
||||
EachState.ID == ID
|
||||
@@ -86,27 +131,17 @@ where Data: Collection, ID: Hashable, Content: View {
|
||||
where
|
||||
EachContent: View,
|
||||
Data == IdentifiedArray<ID, EachState>,
|
||||
Content == WithViewStore<
|
||||
IdentifiedArray<ID, EachState>, (ID, EachAction),
|
||||
ForEach<IdentifiedArray<ID, EachState>, ID, EachContent>
|
||||
>
|
||||
Content == WithViewStore<[ID], (ID, EachAction), ForEach<[ID], ID, EachContent>>
|
||||
{
|
||||
|
||||
self.data = ViewStore(store, removeDuplicates: { _, _ in false }).state
|
||||
let data = store.state.value
|
||||
self.data = data
|
||||
self.content = {
|
||||
WithViewStore(
|
||||
store,
|
||||
removeDuplicates: { lhs, rhs in
|
||||
guard lhs.id == rhs.id else { return false }
|
||||
guard lhs.count == rhs.count else { return false }
|
||||
return zip(lhs, rhs).allSatisfy { $0[keyPath: lhs.id] == $1[keyPath: rhs.id] }
|
||||
}
|
||||
) { viewStore in
|
||||
ForEach(viewStore.state, id: viewStore.id) { element in
|
||||
WithViewStore(store.scope(state: { $0.ids })) { viewStore in
|
||||
ForEach(viewStore.state, id: \.self) { id in
|
||||
content(
|
||||
store.scope(
|
||||
state: { $0[id: element[keyPath: viewStore.id]] ?? element },
|
||||
action: { (element[keyPath: viewStore.id], $0) }
|
||||
state: { $0[id: id] ?? data[id: id]! },
|
||||
action: { (id, $0) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user