import SwiftUI #if canImport(Observation) import Observation #endif #if !os(visionOS) extension Store: Perceptible {} #endif extension Store where State: ObservableState { var observableState: State { self._$observationRegistrar.access(self, keyPath: \.currentState) return self.currentState } /// Direct access to state in the store when `State` conforms to ``ObservableState``. public var state: State { self.observableState } public subscript(dynamicMember keyPath: KeyPath) -> Value { self.state[keyPath: keyPath] } } extension Store: Equatable { public static nonisolated func == (lhs: Store, rhs: Store) -> Bool { lhs === rhs } } extension Store: Hashable { public nonisolated func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } extension Store: Identifiable {} extension Store where State: ObservableState { /// Scopes the store to optional child state and actions. /// /// If your feature holds onto a child feature as an optional: /// /// ```swift /// @Reducer /// struct Feature { /// @ObservableState /// struct State { /// var child: Child.State? /// // ... /// } /// enum Action { /// case child(Child.Action) /// // ... /// } /// // ... /// } /// ``` /// /// …then you can use this `scope` operator in order to transform a store of your feature into /// a non-optional store of the child domain: /// /// ```swift /// if let childStore = store.scope(state: \.child, action: \.child) { /// ChildView(store: childStore) /// } /// ``` /// /// > Important: This operation should only be used from within a SwiftUI view or within /// > `withPerceptionTracking` in order for changes of the optional state to be properly /// > observed. /// /// - Parameters: /// - state: A key path to optional child state. /// - action: A case key path to child actions. /// - Returns: An optional store of non-optional child state and actions. public func scope( state: KeyPath, action: CaseKeyPath, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) -> Store? { if !self.canCacheChildren { reportIssue( uncachedStoreWarning(self), fileID: fileID, filePath: filePath, line: line, column: column ) } guard var childState = self.state[keyPath: state] else { return nil } return self.scope( id: self.id(state: state.appending(path: \.!), action: action), state: ToState { childState = $0[keyPath: state] ?? childState return childState }, action: { action($0) }, isInvalid: { $0[keyPath: state] == nil } ) } } extension Binding { /// Scopes the binding of a store to a binding of an optional presentation store. /// /// Use this operator to derive a binding that can be handed to SwiftUI's various navigation /// view modifiers, such as `sheet(item:)`, `popover(item:)`, etc. /// /// /// For example, suppose your feature can present a child feature in a sheet. Then your feature's /// domain would hold onto the child's domain using the library's presentation tools (see /// for more information on these tools): /// /// ```swift /// @Reducer /// struct Feature { /// @ObservableState /// struct State { /// @Presents var child: Child.State? /// // ... /// } /// enum Action { /// case child(PresentationActionOf) /// // ... /// } /// // ... /// } /// ``` /// /// Then you can derive a binding to the child domain that can be handed to the `sheet(item:)` /// view modifier: /// /// ```swift /// struct FeatureView: View { /// @Bindable var store: StoreOf /// /// var body: some View { /// // ... /// .sheet(item: $store.scope(state: \.child, action: \.child)) { store in /// ChildView(store: store) /// } /// } /// } /// ``` /// /// - Parameters: /// - state: A key path to optional child state. /// - action: A case key path to presentation child actions. /// - Returns: A binding of an optional child store. #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath, action: CaseKeyPath>, fileID: StaticString = #fileID, filePath: StaticString = #fileID, line: UInt = #line, column: UInt = #column ) -> Binding?> where Value == Store { self[ id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), state: state, action: action, isInViewBody: _isInPerceptionTracking, fileID: _HashableStaticString(rawValue: fileID), filePath: _HashableStaticString(rawValue: filePath), line: line, column: column ] } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SwiftUI.Bindable { /// Scopes the binding of a store to a binding of an optional presentation store. /// /// Use this operator to derive a binding that can be handed to SwiftUI's various navigation /// view modifiers, such as `sheet(item:)`, `popover(item:)`, etc. /// /// /// For example, suppose your feature can present a child feature in a sheet. Then your /// feature's domain would hold onto the child's domain using the library's presentation tools /// (see for more information on these tools): /// /// ```swift /// @Reducer /// struct Feature { /// @ObservableState /// struct State { /// @Presents var child: Child.State? /// // ... /// } /// enum Action { /// case child(PresentationActionOf) /// // ... /// } /// // ... /// } /// ``` /// /// Then you can derive a binding to the child domain that can be handed to the `sheet(item:)` /// view modifier: /// /// ```swift /// struct FeatureView: View { /// @Bindable var store: StoreOf /// /// var body: some View { /// // ... /// .sheet(item: $store.scope(state: \.child, action: \.child)) { store in /// ChildView(store: store) /// } /// } /// } /// ``` /// /// - Parameters: /// - state: A key path to optional child state. /// - action: A case key path to presentation child actions. /// - Returns: A binding of an optional child store. #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath, action: CaseKeyPath>, fileID: StaticString = #fileID, filePath: StaticString = #fileID, line: UInt = #line, column: UInt = #column ) -> Binding?> where Value == Store { self[ id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), state: state, action: action, isInViewBody: _isInPerceptionTracking, fileID: _HashableStaticString(rawValue: fileID), filePath: _HashableStaticString(rawValue: filePath), line: line, column: column ] } } @available(iOS, introduced: 13, obsoleted: 17) @available(macOS, introduced: 10.15, obsoleted: 14) @available(tvOS, introduced: 13, obsoleted: 17) @available(watchOS, introduced: 6, obsoleted: 10) extension Perception.Bindable { /// Scopes the binding of a store to a binding of an optional presentation store. /// /// Use this operator to derive a binding that can be handed to SwiftUI's various navigation /// view modifiers, such as `sheet(item:)`, `popover(item:)`, etc. /// /// /// For example, suppose your feature can present a child feature in a sheet. Then your /// feature's domain would hold onto the child's domain using the library's presentation tools /// (see for more information on these tools): /// /// ```swift /// @Reducer /// struct Feature { /// @ObservableState /// struct State { /// @Presents var child: Child.State? /// // ... /// } /// enum Action { /// case child(PresentationActionOf) /// // ... /// } /// // ... /// } /// ``` /// /// Then you can derive a binding to the child domain that can be handed to the `sheet(item:)` /// view modifier: /// /// ```swift /// struct FeatureView: View { /// @Bindable var store: StoreOf /// /// var body: some View { /// // ... /// .sheet(item: $store.scope(state: \.child, action: \.child)) { store in /// ChildView(store: store) /// } /// } /// } /// ``` /// /// - Parameters: /// - state: A key path to optional child state. /// - action: A case key path to presentation child actions. /// - Returns: A binding of an optional child store. public func scope( state: KeyPath, action: CaseKeyPath>, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) -> Binding?> where Value == Store { self[ id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), state: state, action: action, isInViewBody: _isInPerceptionTracking, fileID: _HashableStaticString(rawValue: fileID), filePath: _HashableStaticString(rawValue: filePath), line: line, column: column ] } } extension UIBindable { #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath, action: CaseKeyPath>, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) -> UIBinding?> where Value == Store { self[ id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), state: state, action: action, isInViewBody: _isInPerceptionTracking, fileID: _HashableStaticString(rawValue: fileID), filePath: _HashableStaticString(rawValue: filePath), line: line, column: column ] } } extension Store where State: ObservableState { @_spi(Internals) public subscript( id id: AnyHashable?, state state: KeyPath, action action: CaseKeyPath>, isInViewBody isInViewBody: Bool, fileID fileID: _HashableStaticString, filePath filePath: _HashableStaticString, line line: UInt, column column: UInt ) -> Store? { get { #if DEBUG && !os(visionOS) _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { self.scope( state: state, action: action.appending(path: \.presented), fileID: fileID.rawValue, filePath: filePath.rawValue, line: line, column: column ) } #else self.scope( state: state, action: action.appending(path: \.presented), fileID: fileID.rawValue, filePath: filePath.rawValue, line: line, column: column ) #endif } set { if newValue == nil, let childState = self.state[keyPath: state], id == _identifiableID(childState), !self._isInvalidated() { self.send(action(.dismiss)) if self.state[keyPath: state] != nil { reportIssue( """ A binding at "\(fileID):\(line)" was set to "nil", but the store destination wasn't \ nil'd out. This usually means an "ifLet" has not been integrated with the reducer powering the \ store, and this reducer is responsible for handling presentation actions. To fix this, ensure that "ifLet" is invoked from the reducer's "body": Reduce { state, action in // ... } .ifLet(\\.destination, action: \\.destination) { Destination() } And ensure that every parent reducer is integrated into the root reducer that powers \ the store. """, fileID: fileID.rawValue, filePath: filePath.rawValue, line: line, column: column ) return } } } } } func uncachedStoreWarning(_ store: Store) -> String { """ Scoping from uncached \(store) is not compatible with observation. This can happen for one of two reasons: • A parent view scopes on a store using transform functions, which has been \ deprecated, instead of with key paths and case paths. Read the migration guide for 1.5 \ to update these scopes: https://pointfreeco.github.io/swift-composable-architecture/\ main/documentation/composablearchitecture/migratingto1.5 • A parent feature is using deprecated navigation APIs, such as 'IfLetStore', \ 'SwitchStore', 'ForEachStore', or any navigation view modifiers taking stores instead of \ bindings. Read the migration guide for 1.7 to update those APIs: \ https://pointfreeco.github.io/swift-composable-architecture/main/documentation/\ composablearchitecture/migratingto1.7 """ }