Files
Stephen Celis 149fdf9c20 Use SPI docs instead of self-hosted (#3791)
* Use SPI docs instead of self-hosted

Fixes #3785.

* wip
2025-10-08 09:28:11 -07:00

208 lines
8.1 KiB
Swift

import OrderedCollections
import SwiftUI
/// A navigation stack that is driven by a store.
///
/// This view can be used to drive stack-based navigation in the Composable Architecture when passed
/// a store that is focused on ``StackState`` and ``StackAction``.
///
/// See the dedicated article on <doc:Navigation> for more information on the library's navigation
/// tools, and in particular see <doc:StackBasedNavigation> for information on using this view.
@available(
iOS, deprecated: 9999,
message:
"Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]"
)
@available(
macOS, deprecated: 9999,
message:
"Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]"
)
@available(
tvOS, deprecated: 9999,
message:
"Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]"
)
@available(
watchOS, deprecated: 9999,
message:
"Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]"
)
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public struct NavigationStackStore<State, Action, Root: View, Destination: View>: View {
private let root: Root
private let destination: (StackState<State>.Component) -> Destination
@ObservedObject private var viewStore: ViewStore<StackState<State>, StackAction<State, Action>>
/// Creates a navigation stack with a store of stack state and actions.
///
/// - Parameters:
/// - store: A store of stack state and actions to power this stack.
/// - root: The view to display when the stack is empty.
/// - destination: A view builder that defines a view to display when an element is appended to
/// the stack's state. The closure takes one argument, which is a store of the value to
/// present.
/// - fileID: The fileID.
/// - filePath: The filePath.
/// - line: The line.
/// - column: The column.
public init(
_ store: Store<StackState<State>, StackAction<State, Action>>,
@ViewBuilder root: () -> Root,
@ViewBuilder destination: @escaping (_ store: Store<State, Action>) -> Destination,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) {
func navigationDestination(
component: StackState<State>.Component
) -> Destination {
let id = store.id(
state:
\.[
id: component.id,
fileID: _HashableStaticString(rawValue: fileID),
filePath: _HashableStaticString(rawValue: filePath),
line: line,
column: column
],
action: \.[id: component.id]
)
@MainActor
func open(
_ core: some Core<StackState<State>, StackAction<State, Action>>
) -> any Core<State, Action> {
IfLetCore(
base: core,
cachedState: component.element,
stateKeyPath:
\.[
id: component.id,
fileID: _HashableStaticString(rawValue: fileID),
filePath: _HashableStaticString(rawValue: filePath),
line: line,
column: column
],
actionKeyPath: \.[id: component.id]
)
}
return destination(store.scope(id: id, childCore: open(store.core)))
}
self.root = root()
self.destination = navigationDestination(component:)
self._viewStore = ObservedObject(
wrappedValue: ViewStore(
store,
observe: { $0 },
removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) }
)
)
}
/// Creates a navigation stack with a store of stack state and actions.
///
/// - Parameters:
/// - store: A store of stack state and actions to power this stack.
/// - root: The view to display when the stack is empty.
/// - destination: A view builder that defines a view to display when an element is appended to
/// the stack's state. The closure takes one argument, which is the initial enum state to
/// present. You can switch over this value and use ``CaseLet`` views to handle each case.
/// - fileID: The fileID.
/// - filePath: The filePath.
/// - line: The line.
/// - column: The column.
@_disfavoredOverload
public init<D: View>(
_ store: Store<StackState<State>, StackAction<State, Action>>,
@ViewBuilder root: () -> Root,
@ViewBuilder destination: @escaping (_ initialState: State) -> D,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) where Destination == SwitchStore<State, Action, D> {
func navigationDestination(
component: StackState<State>.Component
) -> Destination {
let id = store.id(
state:
\.[
id: component.id,
fileID: _HashableStaticString(rawValue: fileID),
filePath: _HashableStaticString(rawValue: filePath),
line: line,
column: column
],
action: \.[id: component.id]
)
if let child = store.children[id] as? Store<State, Action> {
return SwitchStore(child, content: destination)
} else {
@MainActor
func open(
_ core: some Core<StackState<State>, StackAction<State, Action>>
) -> any Core<State, Action> {
IfLetCore(
base: core,
cachedState: component.element,
stateKeyPath:
\.[
id: component.id,
fileID: _HashableStaticString(rawValue: fileID),
filePath: _HashableStaticString(rawValue: filePath),
line: line,
column: column
],
actionKeyPath: \.[id: component.id]
)
}
return SwitchStore(store.scope(id: id, childCore: open(store.core)), content: destination)
}
}
self.root = root()
self.destination = navigationDestination(component:)
self._viewStore = ObservedObject(
wrappedValue: ViewStore(
store,
observe: { $0 },
removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) }
)
)
}
public var body: some View {
NavigationStack(
path: self.viewStore.binding(
get: { $0.path },
compactSend: { newPath in
if newPath.count > self.viewStore.path.count, let component = newPath.last {
return .push(id: component.id, state: component.element)
} else if newPath.count < self.viewStore.path.count {
return .popFrom(id: self.viewStore.path[newPath.count].id)
} else {
return nil
}
}
)
) {
self.root
.environment(\.navigationDestinationType, State.self)
.navigationDestination(for: StackState<State>.Component.self) { component in
NavigationDestinationView(component: component, destination: self.destination)
}
}
}
}
private struct NavigationDestinationView<State, Destination: View>: View {
let component: StackState<State>.Component
let destination: (StackState<State>.Component) -> Destination
var body: some View {
self.destination(self.component)
.environment(\.navigationDestinationType, State.self)
.id(self.component.id)
}
}