import SwiftUI extension Binding { /// Derives a binding to a store focused on ``StackState`` and ``StackAction``. /// /// This operator is most used in conjunction with `NavigationStack`, and in particular /// the initializer ``SwiftUI/NavigationStack/init(path:root:destination:fileID:filePath:line:column:)`` that /// ships with this library. /// /// For example, suppose you have a feature that holds onto ``StackState`` in its state in order /// to represent all the screens that can be pushed onto a navigation stack: /// /// ```swift /// @Reducer /// struct Feature { /// @ObservableState /// struct State { /// var path: StackState = [] /// } /// enum Action { /// case path(StackActionOf) /// } /// var body: some ReducerOf { /// Reduce { state, action in /// // Core feature logic /// } /// .forEach(\.rows, action: \.rows) { /// Child() /// } /// } /// @Reducer /// enum Path { /// // ... /// } /// } /// ``` /// /// > Note: We are using the ``Reducer()`` macro on an enum to compose together all the features /// that can be pushed onto the stack. See for /// more information. /// /// Then in the view you can use this operator, with /// `NavigationStack` ``SwiftUI/NavigationStack/init(path:root:destination:fileID:filePath:line:column:)``, to /// derive a store for each element in the stack: /// /// ```swift /// struct FeatureView: View { /// @Bindable var store: StoreOf /// /// var body: some View { /// NavigationStack(path: $store.scope(state: \.path, action: \.path)) { /// // Root view /// } destination: { /// // Destinations /// } /// } /// } /// ``` #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath>, action: CaseKeyPath> ) -> Binding, StackAction>> where Value == Store { self[state: state, action: action] } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SwiftUI.Bindable { /// Derives a binding to a store focused on ``StackState`` and ``StackAction``. /// /// See ``SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:)`` defined on `Binding` for more /// information. #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath>, action: CaseKeyPath> ) -> Binding, StackAction>> where Value == Store { self[state: state, action: action] } } @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 { /// Derives a binding to a store focused on ``StackState`` and ``StackAction``. /// /// See ``SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:)`` defined on `Binding` for more /// information. public func scope( state: KeyPath>, action: CaseKeyPath> ) -> Binding, StackAction>> where Value == Store { self[state: state, action: action] } } extension UIBindable { /// Derives a binding to a store focused on ``StackState`` and ``StackAction``. /// /// See ``SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:)`` defined on `Binding` for more /// information. #if swift(>=5.10) @preconcurrency@MainActor #else @MainActor(unsafe) #endif public func scope( state: KeyPath>, action: CaseKeyPath> ) -> UIBinding, StackAction>> where Value == Store { self[state: state, action: action] } } @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) extension NavigationStack { /// Drives a navigation stack with a store. /// /// See the dedicated article on for more information on the library's /// navigation tools, and in particular see for information on using /// this view. public init( path: Binding, StackAction>>, root: () -> R, @ViewBuilder destination: @escaping (Store) -> Destination, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) where Data == StackState.PathView, Root == ModifiedContent> { self.init( path: path[ fileID: _HashableStaticString(rawValue: fileID), filePath: _HashableStaticString(rawValue: filePath), line: line, column: column ] ) { root() .modifier( _NavigationDestinationViewModifier( store: path.wrappedValue, destination: destination, fileID: fileID, filePath: filePath, line: line, column: column ) ) } } } @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) public struct _NavigationDestinationViewModifier< State: ObservableState, Action, Destination: View >: ViewModifier { @SwiftUI.State var store: Store, StackAction> fileprivate let destination: (Store) -> Destination fileprivate let fileID: StaticString fileprivate let filePath: StaticString fileprivate let line: UInt fileprivate let column: UInt public func body(content: Content) -> some View { content .environment(\.navigationDestinationType, State.self) .navigationDestination(for: StackState.Component.self) { component in var element = component.element self .destination( self.store.scope( id: self.store.id( state: \.[ id: component.id, fileID: _HashableStaticString( rawValue: fileID), filePath: _HashableStaticString( rawValue: filePath), line: line, column: column ], action: \.[id: component.id] ), state: ToState { element = $0[id: component.id] ?? element return element }, action: { .element(id: component.id, action: $0) }, isInvalid: { !$0.ids.contains(component.id) } ) ) .environment(\.navigationDestinationType, State.self) } } } @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) extension NavigationLink where Destination == Never { /// Creates a navigation link that presents the view corresponding to an element of /// ``StackState``. /// /// When someone activates the navigation link that this initializer creates, SwiftUI looks for /// a parent `NavigationStack` view with a store of ``StackState`` containing elements that /// matches the type of this initializer's `state` input. /// /// See SwiftUI's documentation for `NavigationLink.init(value:label:)` for more. /// /// - Parameters: /// - state: An optional value to present. When the user selects the link, SwiftUI stores a /// copy of the value. Pass a `nil` value to disable the link. /// - label: A label that describes the view that this link presents. public init( state: P?, @ViewBuilder label: () -> L, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) where Label == _NavigationLinkStoreContent { @Dependency(\.stackElementID) var stackElementID self.init(value: state.map { StackState.Component(id: stackElementID(), element: $0) }) { _NavigationLinkStoreContent( state: state, label: label, fileID: fileID, filePath: filePath, line: line, column: column ) } } /// Creates a navigation link that presents the view corresponding to an element of /// ``StackState``, with a text label that the link generates from a localized string key. /// /// When someone activates the navigation link that this initializer creates, SwiftUI looks for /// a parent ``NavigationStackStore`` view with a store of ``StackState`` containing elements /// that matches the type of this initializer's `state` input. /// /// See SwiftUI's documentation for `NavigationLink.init(_:value:)` for more. /// /// - Parameters: /// - titleKey: A localized string that describes the view that this link /// presents. /// - state: An optional value to present. When the user selects the link, SwiftUI stores a /// copy of the value. Pass a `nil` value to disable the link. public init

( _ titleKey: LocalizedStringKey, state: P?, fileID: StaticString = #fileID, line: UInt = #line ) where Label == _NavigationLinkStoreContent { self.init(state: state, label: { Text(titleKey) }, fileID: fileID, line: line) } /// Creates a navigation link that presents the view corresponding to an element of /// ``StackState``, with a text label that the link generates from a title string. /// /// When someone activates the navigation link that this initializer creates, SwiftUI looks for /// a parent ``NavigationStackStore`` view with a store of ``StackState`` containing elements /// that matches the type of this initializer's `state` input. /// /// See SwiftUI's documentation for `NavigationLink.init(_:value:)` for more. /// /// - Parameters: /// - title: A string that describes the view that this link presents. /// - state: An optional value to present. When the user selects the link, SwiftUI stores a /// copy of the value. Pass a `nil` value to disable the link. @_disfavoredOverload public init( _ title: S, state: P?, fileID: StaticString = #fileID, line: UInt = #line ) where Label == _NavigationLinkStoreContent { self.init(state: state, label: { Text(title) }, fileID: fileID, line: line) } } public struct _NavigationLinkStoreContent: View { let state: State? @ViewBuilder let label: Label let fileID: StaticString let filePath: StaticString let line: UInt let column: UInt @Environment(\.navigationDestinationType) var navigationDestinationType @_spi(Internals) public init( state: State?, @ViewBuilder label: () -> Label, fileID: StaticString, filePath: StaticString, line: UInt, column: UInt ) { self.state = state self.label = label() self.fileID = fileID self.filePath = filePath self.line = line self.column = column } public var body: some View { #if DEBUG label.onAppear { if navigationDestinationType != State.self { let elementType = navigationDestinationType.map { typeName($0) } ?? """ (None found in view hierarchy. Is this link inside a store-powered \ 'NavigationStack'?) """ reportIssue( """ A navigation link at "\(fileID):\(line)" is unpresentable. … NavigationStack state element type: \(elementType) NavigationLink state type: \(typeName(State.self)) NavigationLink state value: \(String(customDumping: state).indent(by: 2)) """, fileID: fileID, filePath: filePath, line: line, column: column ) } } #else label #endif } } extension Store where State: ObservableState { fileprivate subscript( state state: KeyPath>, action action: CaseKeyPath>, isInViewBody isInViewBody: Bool = _isInPerceptionTracking ) -> Store, StackAction> { get { #if DEBUG && !os(visionOS) _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { self.scope(state: state, action: action) } #else self.scope(state: state, action: action) #endif } set {} } } extension Store { @_spi(Internals) public subscript( fileID fileID: _HashableStaticString, filePath filePath: _HashableStaticString, line line: UInt, column column: UInt ) -> StackState.PathView where State == StackState, Action == StackAction { get { self.currentState.path } set { let newCount = newValue.count guard newCount != self.currentState.count else { reportIssue( """ A navigation stack binding at "\(fileID.rawValue):\(line)" was written to with a \ path that has the same number of elements that already exist in the store. A view \ should only write to this binding with a path that has pushed a new element onto the \ stack, or popped one or more elements from the stack. This usually means the "forEach" has not been integrated with the reducer powering the \ store, and this reducer is responsible for handling stack actions. To fix this, ensure that "forEach" is invoked from the reducer's "body": Reduce { state, action in // ... } .forEach(\\.path, action: \\.path) { Path() } 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 } if newCount > self.currentState.count, let component = newValue.last { self.send(.push(id: component.id, state: component.element)) } else { self.send(.popFrom(id: self.currentState.ids[newCount])) } } } } @_spi(Internals) public var _isInPerceptionTracking: Bool { #if !os(visionOS) return _PerceptionLocals.isInPerceptionTracking #else return false #endif } extension StackState { var path: PathView { _read { yield PathView(base: self) } _modify { var path = PathView(base: self) yield &path self = path.base } set { self = newValue.base } } public struct Component: Hashable { @_spi(Internals) public let id: StackElementID @_spi(Internals) public var element: Element @_spi(Internals) public init(id: StackElementID, element: Element) { self.id = id self.element = element } public static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } public func hash(into hasher: inout Hasher) { hasher.combine(self.id) } } public struct PathView: MutableCollection, RandomAccessCollection, RangeReplaceableCollection { var base: StackState public var startIndex: Int { self.base.startIndex } public var endIndex: Int { self.base.endIndex } public func index(after i: Int) -> Int { self.base.index(after: i) } public func index(before i: Int) -> Int { self.base.index(before: i) } public subscript(position: Int) -> Component { _read { yield Component(id: self.base.ids[position], element: self.base[position]) } _modify { let id = self.base.ids[position] var component = Component(id: id, element: self.base[position]) yield &component self.base[id: id] = component.element } set { self.base[id: newValue.id] = newValue.element } } init(base: StackState) { self.base = base } public init() { self.init(base: StackState()) } public mutating func replaceSubrange( _ subrange: Range, with newElements: some Collection ) { for id in self.base.ids[subrange] { self.base[id: id] = nil } for component in newElements.reversed() { self.base._dictionary .updateValue(component.element, forKey: component.id, insertingAt: subrange.lowerBound) } } } } private struct NavigationDestinationTypeKey: EnvironmentKey { static var defaultValue: Any.Type? { nil } } extension EnvironmentValues { @_spi(Internals) public var navigationDestinationType: Any.Type? { get { self[NavigationDestinationTypeKey.self] } set { self[NavigationDestinationTypeKey.self] = newValue } } }