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 /// @Reducer /// struct Todo { /// struct State: Equatable, Identifiable { /// let id: UUID /// var description = "" /// var isComplete = false /// } /// /// enum Action { /// case isCompleteToggled(Bool) /// case descriptionChanged(String) /// } /// /// var body: some Reducer { /// // ... /// } /// } /// ``` /// /// 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 /// @Reducer /// struct Todos { /// struct State: Equatable { /// var todos: IdentifiedArrayOf = [] /// } /// // ... /// } /// ``` /// /// Define a case to handle actions sent to the child domain: /// /// ```swift /// enum Action { /// case todos(IdentifiedActionOf) /// } /// ``` /// /// Enhance its core reducer using /// ``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-6zye8``: /// /// ```swift /// var body: some Reducer { /// Reduce { state, action in /// // ... /// } /// .forEach(\.todos, action: \.todos) { /// Todo() /// } /// } /// ``` /// /// And finally render a list of `TodoView`s using ``ForEachStore``: /// /// ```swift /// ForEachStore( /// self.store.scope(state: \.todos, action: \.todos) /// ) { todoStore in /// TodoView(store: todoStore) /// } /// ``` /// @available( iOS, deprecated: 9999, message: "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" ) @available( macOS, deprecated: 9999, message: "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" ) @available( tvOS, deprecated: 9999, message: "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" ) @available( watchOS, deprecated: 9999, message: "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" ) public struct ForEachStore< EachState, EachAction, Data: Collection, ID: Hashable & Sendable, Content: View >: View { 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. #if swift(<5.10) @MainActor(unsafe) #else @preconcurrency@MainActor #endif public init( _ store: Store, IdentifiedAction>, @ViewBuilder content: @escaping (_ store: Store) -> EachContent ) where Data == IdentifiedArray, Content == WithViewStore< IdentifiedArray, IdentifiedAction, ForEach, ID, EachContent> > { self.data = store.withState { $0 } func open( _ core: some Core, IdentifiedAction>, element: EachState, id: ID ) -> any Core { IfLetCore( base: core, cachedState: element, stateKeyPath: \.[id: id], actionKeyPath: \.[id: id] ) } self.content = WithViewStore( store, observe: { $0 }, removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } ) { viewStore in ForEach(viewStore.state, id: viewStore.state.id) { element in let id = element[keyPath: viewStore.state.id] content( store.scope( id: store.id(state: \.[id: id]!, action: \.[id: id]), childCore: open(store.core, element: element, id: id) ) ) } } } @available( iOS, deprecated: 9999, message: "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" ) @available( macOS, deprecated: 9999, message: "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" ) @available( tvOS, deprecated: 9999, message: "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" ) @available( watchOS, deprecated: 9999, message: "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" ) #if swift(<5.10) @MainActor(unsafe) #else @preconcurrency@MainActor #endif public init( _ store: Store, (id: ID, action: EachAction)>, @ViewBuilder content: @escaping (_ store: Store) -> EachContent ) where Data == IdentifiedArray, Content == WithViewStore< IdentifiedArray, (id: ID, action: EachAction), ForEach, ID, EachContent> > { self.data = store.withState { $0 } func open( _ core: some Core, (id: ID, action: EachAction)>, element: EachState, id: ID ) -> any Core { IfLetCore( base: core, cachedState: element, stateKeyPath: \.[id: id], actionKeyPath: \.[id: id] ) } self.content = WithViewStore( store, observe: { $0 }, removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } ) { viewStore in ForEach(viewStore.state, id: viewStore.state.id) { element in let id = element[keyPath: viewStore.state.id] content( store.scope( id: store.id(state: \.[id: id]!, action: \.[id: id]), childCore: open(store.core, element: element, id: id) ) ) } } } public var body: some View { self.content } } #if compiler(>=6) extension ForEachStore: @preconcurrency DynamicViewContent {} #else extension ForEachStore: DynamicViewContent {} #endif extension Case { fileprivate subscript(id id: ID) -> Case where Value == (id: ID, action: Action) { Case( embed: { (id: id, action: $0) }, extract: { $0.id == id ? $0.action : nil } ) } }