import Combine import CombineSchedulers import Foundation import SwiftUI /// A store represents the runtime that powers the application. It is the object that you will pass /// around to views that need to interact with the application. /// /// You will typically construct a single one of these at the root of your application: /// /// ```swift /// @main /// struct MyApp: App { /// var body: some Scene { /// WindowGroup { /// RootView( /// store: Store(initialState: AppFeature.State()) { /// AppFeature() /// } /// ) /// } /// } /// } /// ``` /// /// …and then use the ``scope(state:action:)-90255`` method to derive more focused stores that can be /// passed to subviews. /// /// ### Scoping /// /// The most important operation defined on ``Store`` is the ``scope(state:action:)-90255`` method, /// which allows you to transform a store into one that deals with child state and actions. This is /// necessary for passing stores to subviews that only care about a small portion of the entire /// application's domain. /// /// For example, if an application has a tab view at its root with tabs for activity, search, and /// profile, then we can model the domain like this: /// /// ```swift /// @Reducer /// struct AppFeature { /// struct State { /// var activity: Activity.State /// var profile: Profile.State /// var search: Search.State /// } /// /// enum Action { /// case activity(Activity.Action) /// case profile(Profile.Action) /// case search(Search.Action) /// } /// /// // ... /// } /// ``` /// /// We can construct a view for each of these domains by applying ``scope(state:action:)-90255`` to /// a store that holds onto the full app domain in order to transform it into a store for each /// subdomain: /// /// ```swift /// struct AppView: View { /// let store: StoreOf /// /// var body: some View { /// TabView { /// ActivityView( /// store: store.scope(state: \.activity, action: \.activity) /// ) /// .tabItem { Text("Activity") } /// /// SearchView( /// store: store.scope(state: \.search, action: \.search) /// ) /// .tabItem { Text("Search") } /// /// ProfileView( /// store: store.scope(state: \.profile, action: \.profile) /// ) /// .tabItem { Text("Profile") } /// } /// } /// } /// ``` /// /// ### ObservableObject conformance /// /// The store conforms to `ObservableObject` but is _not_ observable via the `@ObservedObject` /// property wrapper. This conformance is completely inert and its sole purpose is to allow stores /// to be held in SwiftUI's `@StateObject` property wrapper. /// /// Instead, stores should be observed through Swift's Observation framework (or the Perception /// package when targeting iOS <17) by applying the ``ObservableState()`` macro to your feature's /// state. @dynamicMemberLookup #if swift(<5.10) @MainActor(unsafe) #else @preconcurrency@MainActor #endif public final class Store: _Store { var children: [ScopeID: AnyObject] = [:] private weak var parent: (any _Store)? private let scopeID: AnyHashable? func removeChild(scopeID: AnyHashable) { children[scopeID as! ScopeID] = nil } let core: any Core @_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] { core.effectCancellables } #if !os(visionOS) let _$observationRegistrar = PerceptionRegistrar( isPerceptionCheckingEnabled: _isStorePerceptionCheckingEnabled ) #else let _$observationRegistrar = ObservationRegistrar() #endif private var parentCancellable: AnyCancellable? /// Initializes a store from an initial state and a reducer. /// /// - Parameters: /// - initialState: The state to start the application in. /// - reducer: The reducer that powers the business logic of the application. /// - prepareDependencies: A closure that can be used to override dependencies that will be accessed /// by the reducer. public convenience init>( initialState: @autoclosure () -> R.State, @ReducerBuilder reducer: () -> R, withDependencies prepareDependencies: ((inout DependencyValues) -> Void)? = nil ) { let (initialState, reducer, dependencies) = withDependencies(prepareDependencies ?? { _ in }) { @Dependency(\.self) var dependencies var updatedDependencies = dependencies updatedDependencies.navigationIDPath.append(NavigationID()) return (initialState(), reducer(), updatedDependencies) } self.init( initialState: initialState, reducer: reducer.dependency(\.self, dependencies) ) } init() { self.core = InvalidCore() self.scopeID = nil } deinit { guard Thread.isMainThread else { return } MainActor._assumeIsolated { Logger.shared.log("\(storeTypeName(of: self)).deinit") } } /// Calls the given closure with a snapshot of the current state of the store. /// /// A lightweight way of accessing store state when state is not observable and ``state-1qxwl`` is /// unavailable. /// /// - Parameter body: A closure that takes the current state of the store as its sole argument. If /// the closure has a return value, that value is also used as the return value of the /// `withState` method. The state argument reflects the current state of the store only for the /// duration of the closure's execution, and is only observable over time, _e.g._ by SwiftUI, if /// it conforms to ``ObservableState``. /// - Returns: The return value, if any, of the `body` closure. public func withState(_ body: (_ state: State) -> R) -> R { #if DEBUG _PerceptionLocals.$skipPerceptionChecking.withValue(true) { body(self.currentState) } #else body(self.currentState) #endif } /// Sends an action to the store. /// /// This method returns a ``StoreTask``, which represents the lifecycle of the effect started from /// sending an action. You can use this value to tie the effect's lifecycle _and_ cancellation to /// an asynchronous context, such as SwiftUI's `task` view modifier: /// /// ```swift /// .task { await store.send(.task).finish() } /// ``` /// /// - Parameter action: An action. /// - Returns: A ``StoreTask`` that represents the lifecycle of the effect executed when /// sending the action. @discardableResult public func send(_ action: Action) -> StoreTask { .init(rawValue: self.send(action)) } /// Sends an action to the store with a given animation. /// /// See ``Store/send(_:)`` for more info. /// /// - Parameters: /// - action: An action. /// - animation: An animation. @discardableResult public func send(_ action: Action, animation: Animation?) -> StoreTask { send(action, transaction: Transaction(animation: animation)) } /// Sends an action to the store with a given transaction. /// /// See ``Store/send(_:)`` for more info. /// /// - Parameters: /// - action: An action. /// - transaction: A transaction. @discardableResult public func send(_ action: Action, transaction: Transaction) -> StoreTask { withTransaction(transaction) { .init(rawValue: self.send(action)) } } /// Scopes the store to one that exposes child state and actions. /// /// This can be useful for deriving new stores to hand to child views in an application. For /// example: /// /// ```swift /// @Reducer /// struct AppFeature { /// @ObservableState /// struct State { /// var login: Login.State /// // ... /// } /// enum Action { /// case login(Login.Action) /// // ... /// } /// // ... /// } /// /// // A store that runs the entire application. /// let store = Store(initialState: AppFeature.State()) { /// AppFeature() /// } /// /// // Construct a login view by scoping the store /// // to one that works with only login domain. /// LoginView( /// store: store.scope(state: \.login, action: \.login) /// ) /// ``` /// /// Scoping in this fashion allows you to better modularize your application. In this case, /// `LoginView` could be extracted to a module that has no access to `AppFeature.State` or /// `AppFeature.Action`. /// /// - Parameters: /// - state: A key path from `State` to `ChildState`. /// - action: A case key path from `Action` to `ChildAction`. /// - Returns: A new store with its domain (state and action) transformed. public func scope( state: KeyPath, action: CaseKeyPath ) -> Store { func open(_ core: some Core) -> any Core { ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) } return scope(id: id(state: state, action: action), childCore: open(core)) } func scope( id: ScopeID?, childCore: @autoclosure () -> any Core ) -> Store { guard core.canStoreCacheChildren, let id, let child = children[id] as? Store else { let child = Store(core: childCore(), scopeID: id, parent: self) if core.canStoreCacheChildren, let id { children[id] = child } return child } return child } @available( *, deprecated, message: "Pass 'state' a key path to child state and 'action' a case key path to child action, instead. For more information see the following migration guide: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Store-scoping-with-key-paths" ) public func scope( state toChildState: @escaping (_ state: State) -> ChildState, action fromChildAction: @escaping (_ childAction: ChildAction) -> Action ) -> Store { _scope(state: toChildState, action: fromChildAction) } func _scope( state toChildState: @escaping (_ state: State) -> ChildState, action fromChildAction: @escaping (_ childAction: ChildAction) -> Action ) -> Store { func open(_ core: some Core) -> any Core { ClosureScopedCore( base: core, toState: toChildState, fromAction: fromChildAction ) } return scope(id: nil, childCore: open(core)) } @_spi(Internals) public var currentState: State { core.state } @_spi(Internals) @_disfavoredOverload public func send(_ action: Action) -> Task? { core.send(action) } private init(core: some Core, scopeID: AnyHashable?, parent: (any _Store)?) { defer { Logger.shared.log("\(storeTypeName(of: self)).init") } self.core = core self.parent = parent self.scopeID = scopeID if let stateType = State.self as? any ObservableState.Type { func subscribeToDidSet(_ type: T.Type) -> AnyCancellable { return core.didSet .prefix { [weak self] _ in self?.core.isInvalid == false } .compactMap { [weak self] in (self?.withState(\.self) as? T)?._$id } .removeDuplicates() .dropFirst() .sink { [weak self, weak parent] _ in guard let scopeID = self?.scopeID else { return } DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { parent?.removeChild(scopeID: scopeID) } } receiveValue: { [weak self] _ in guard let self else { return } self._$observationRegistrar.withMutation(of: self, keyPath: \.currentState) {} } } self.parentCancellable = subscribeToDidSet(stateType) } } convenience init>( initialState: R.State, reducer: R ) { self.init( core: RootCore(initialState: initialState, reducer: reducer), scopeID: nil, parent: nil ) } /// A publisher that emits when state changes. /// /// This publisher supports dynamic member lookup so that you can pluck out a specific field in /// the state: /// /// ```swift /// store.publisher.alert /// .sink { ... } /// ``` public var publisher: StorePublisher { StorePublisher( store: self, upstream: self.core.didSet.receive(on: UIScheduler.shared).map { self.withState(\.self) } ) } @_spi(Internals) public func id( state: KeyPath, action: CaseKeyPath ) -> ScopeID { ScopeID(state: state, action: action) } } @_spi(Internals) public struct ScopeID: Hashable { let state: PartialKeyPath let action: PartialCaseKeyPath } extension Store: CustomDebugStringConvertible { public nonisolated var debugDescription: String { storeTypeName(of: self) } } extension Store: ObservableObject {} /// A convenience type alias for referring to a store of a given reducer's domain. /// /// Instead of specifying two generics: /// /// ```swift /// let store: Store /// ``` /// /// You can specify a single generic: /// /// ```swift /// let store: StoreOf /// ``` public typealias StoreOf = Store /// A publisher of store state. @dynamicMemberLookup public struct StorePublisher: Publisher { public typealias Output = State public typealias Failure = Never let store: Any let upstream: AnyPublisher init(store: Any, upstream: some Publisher) { self.store = store self.upstream = upstream.eraseToAnyPublisher() } public func receive(subscriber: some Subscriber) { self.upstream.subscribe( AnySubscriber( receiveSubscription: subscriber.receive(subscription:), receiveValue: { value in _PerceptionLocals.$skipPerceptionChecking.withValue(true) { subscriber.receive(value) } }, receiveCompletion: { [store = self.store] in subscriber.receive(completion: $0) _ = store } ) ) } /// Returns the resulting publisher of a given key path. public subscript( dynamicMember keyPath: KeyPath ) -> StorePublisher { .init(store: self.store, upstream: self.upstream.map(keyPath).removeDuplicates()) } } /// The type returned from ``Store/send(_:)`` that represents the lifecycle of the effect /// started from sending an action. /// /// You can use this value to tie the effect's lifecycle _and_ cancellation to an asynchronous /// context, such as the `task` view modifier. /// /// ```swift /// .task { await store.send(.task).finish() } /// ``` /// /// > Note: Unlike Swift's `Task` type, ``StoreTask`` automatically sets up a cancellation /// > handler between the current async context and the task. /// /// See ``TestStoreTask`` for the analog returned from ``TestStore``. public struct StoreTask: Hashable, Sendable { internal let rawValue: Task? internal init(rawValue: Task?) { self.rawValue = rawValue } /// Cancels the underlying task. public func cancel() { self.rawValue?.cancel() } /// Waits for the task to finish. public func finish() async { await self.rawValue?.cancellableValue } /// A Boolean value that indicates whether the task should stop executing. /// /// After the value of this property becomes `true`, it remains `true` indefinitely. There is no /// way to uncancel a task. public var isCancelled: Bool { self.rawValue?.isCancelled ?? true } } func storeTypeName(of store: Store) -> String { let stateType = typeName(State.self, genericsAbbreviated: false) let actionType = typeName(Action.self, genericsAbbreviated: false) if stateType.hasSuffix(".State"), actionType.hasSuffix(".Action"), stateType.dropLast(6) == actionType.dropLast(7) { return "StoreOf<\(stateType.dropLast(6))>" } else if stateType.hasSuffix(".State?"), actionType.hasSuffix(".Action"), stateType.dropLast(7) == actionType.dropLast(7) { return "StoreOf<\(stateType.dropLast(7))?>" } else if stateType.hasPrefix("IdentifiedArray<"), actionType.hasPrefix("IdentifiedAction<"), stateType.dropFirst(16).dropLast(7) == actionType.dropFirst(17).dropLast(8) { return "IdentifiedStoreOf<\(stateType.drop(while: { $0 != "," }).dropFirst(2).dropLast(7))>" } else if stateType.hasPrefix("PresentationState<"), actionType.hasPrefix("PresentationAction<"), stateType.dropFirst(18).dropLast(7) == actionType.dropFirst(19).dropLast(8) { return "PresentationStoreOf<\(stateType.dropFirst(18).dropLast(7))>" } else if stateType.hasPrefix("StackState<"), actionType.hasPrefix("StackAction<"), stateType.dropFirst(11).dropLast(7) == actionType.dropFirst(12).prefix(while: { $0 != "," }).dropLast(6) { return "StackStoreOf<\(stateType.dropFirst(11).dropLast(7))>" } else { return "Store<\(stateType), \(actionType)>" } } // NB: From swift-custom-dump. Consider publicizing interface in some way to keep things in sync. @usableFromInline func typeName( _ type: Any.Type, qualified: Bool = true, genericsAbbreviated: Bool = true ) -> String { var name = _typeName(type, qualified: qualified) .replacingOccurrences( of: #"\(unknown context at \$[[:xdigit:]]+\)\."#, with: "", options: .regularExpression ) for _ in 1...10 { // NB: Only handle so much nesting let abbreviated = name .replacingOccurrences( of: #"\bSwift.Optional<([^><]+)>"#, with: "$1?", options: .regularExpression ) .replacingOccurrences( of: #"\bSwift.Array<([^><]+)>"#, with: "[$1]", options: .regularExpression ) .replacingOccurrences( of: #"\bSwift.Dictionary<([^,<]+), ([^><]+)>"#, with: "[$1: $2]", options: .regularExpression ) if abbreviated == name { break } name = abbreviated } name = name.replacingOccurrences( of: #"\w+\.([\w.]+)"#, with: "$1", options: .regularExpression ) if genericsAbbreviated { name = name.replacingOccurrences( of: #"<.+>"#, with: "", options: .regularExpression ) } return name } let _isStorePerceptionCheckingEnabled: Bool = { if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return false } else { return true } }() #if canImport(Observation) // NB: This extension must be placed in the same file as 'class Store' due to either a bug // in Swift, or very opaque and undocumented behavior of Swift. // See https://github.com/tuist/tuist/issues/6320#issuecomment-2148554117 @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension Store: Observable {} #endif @MainActor private protocol _Store: AnyObject { func removeChild(scopeID: AnyHashable) }