import OrderedCollections import SwiftUI /// 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: /// /// ```swift /// struct Todo: Reducer { /// struct State: Equatable, Identifiable { /// let id: UUID /// var description = "" /// var isComplete = false /// } /// /// enum Action { /// case isCompleteToggled(Bool) /// case descriptionChanged(String) /// } /// /// func reduce(into state: inout State, action: Action) -> Effect { /// // ... /// } /// } /// ``` /// /// As well as a view with a domain-specific store: /// /// ```swift /// struct TodoView: View { /// let store: StoreOf /// var body: some View { /* ... */ } /// } /// ``` /// /// For a parent domain to work with a collection of todos, it can hold onto this collection in /// state: /// /// ```swift /// struct Todos: Reducer { /// struct State: Equatable { /// var todos: IdentifiedArrayOf = [] /// } /// // ... /// } /// ``` /// /// Define a case to handle actions sent to the child domain: /// /// ```swift /// enum Action { /// case todo(id: Todo.State.ID, action: Todo.Action) /// } /// ``` /// /// Enhance its core reducer using ``Reducer/forEach(_:action:element:fileID:line:)``: /// /// ```swift /// var body: some Reducer { /// Reduce { state, action in /// // ... /// } /// .forEach(\.todos, action: /Action.todo(id:action:)) { /// Todo() /// } /// } /// ``` /// /// And finally render a list of `TodoView`s using ``ForEachStore``: /// /// ```swift /// ForEachStore( /// self.store.scope(state: \.todos, action: AppAction.todo(id:action:)) /// ) { todoStore in /// TodoView(store: todoStore) /// } /// ``` /// public struct ForEachStore< EachState, EachAction, Data: Collection, ID: Hashable, Content: View >: DynamicViewContent { public let data: Data let content: Content /// Initializes a structure that computes views on demand from a store on a collection of data and /// an identified action. /// /// - Parameters: /// - store: A store on an identified array of data and an identified action. /// - content: A function that can generate content given a store of an element. public init( _ store: Store, (ID, EachAction)>, @ViewBuilder content: @escaping (_ store: Store) -> EachContent ) where Data == IdentifiedArray, Content == WithViewStore< IdentifiedArray, (ID, EachAction), ForEach, ID, EachContent> > { self.data = store.state.value self.content = WithViewStore( store, observe: { $0 }, removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } ) { viewStore in ForEach(viewStore.state, id: viewStore.state.id) { element in var element = element let id = element[keyPath: viewStore.state.id] content( store.scope( state: { element = $0[id: id] ?? element return element }, action: { (id, $0) } ) ) } } } public var body: some View { self.content } }