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 TodoState: Equatable, Identifiable { /// let id: UUID /// var description = "" /// var isComplete = false /// } /// enum TodoAction { /// case isCompleteToggled(Bool) /// case descriptionChanged(String) /// } /// struct TodoEnvironment {} /// let todoReducer = Reducer /// 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 AppState: Equatable { /// var todos: IdentifiedArrayOf = [] /// } /// ``` /// /// Define a case to handle actions sent to the child domain: /// /// ```swift /// enum AppAction { /// case todo(id: TodoState.ID, action: TodoAction) /// } /// ``` /// /// Enhance its reducer using ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87``: /// /// ```swift /// let appReducer = todoReducer.forEach( /// state: \.todos, /// action: /AppAction.todo(id:action:), /// environment: { _ in TodoEnvironment() } /// ) /// ``` /// /// And finally render a list of `TodoView`s using ``ForEachStore``: /// /// ```swift /// ForEachStore( /// self.store.scope(state: \.todos, AppAction.todo(id:action:)) /// ) { todoStore in /// TodoView(store: todoStore) /// } /// ``` /// public struct ForEachStore: DynamicViewContent where Data: Collection, ID: Hashable, Content: View { public let data: Data private let content: () -> Content /// Initializes a structure that computes views on demand from a store on an array of data and an /// indexed action. /// /// - Parameters: /// - store: A store on an array of data and an indexed action. /// - id: A key path identifying an element. /// - content: A function that can generate content given a store of an element. public init( _ store: Store, id: KeyPath, @ViewBuilder content: @escaping (Store) -> EachContent ) where Data == [EachState], EachContent: View, Content == WithViewStore< [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> > { let data = store.state.value self.data = data self.content = { 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] : data[index] }, action: { (index, $0) } ) ) } } } } /// Initializes a structure that computes views on demand from a store on an array of data and an /// indexed action. /// /// - Parameters: /// - store: A store on an array of data and an indexed action. /// - content: A function that can generate content given a store of an element. public init( _ store: Store, @ViewBuilder content: @escaping (Store) -> EachContent ) where Data == [EachState], EachContent: View, Content == WithViewStore< [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> >, EachState: Identifiable, EachState.ID == ID { self.init(store, id: \.id, 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) -> EachContent ) where EachContent: View, Data == IdentifiedArray, Content == WithViewStore<[ID], (ID, EachAction), ForEach<[ID], ID, EachContent>> { self.data = store.state.value self.content = { WithViewStore(store.scope(state: { $0.ids })) { viewStore in ForEach(viewStore.state, id: \.self) { id -> EachContent in // NB: We cache elements here to avoid a potential crash where SwiftUI may re-evaluate // views for elements no longer in the collection. // // Feedback filed: https://gist.github.com/stephencelis/cdf85ae8dab437adc998fb0204ed9a6b let element = store.state.value[id: id]! return content( store.scope( state: { $0[id: id] ?? element }, action: { (id, $0) } ) ) } } } } public var body: some View { self.content() } }