GraniteNavigation.Router
public final class GraniteNavigation: ObservableObject {
public struct Router {
public let id: String
public init(id: String) {
self.id = id
}
public var navigation: GraniteNavigation {
GraniteNavigation.instance(for: id)
}
public func push<C: View>(style: GraniteNavigationDestinationStyle = .init(),
window: GraniteRouteWindowProperties? = nil,
@ViewBuilder _ content: @escaping () -> C) {
self.navigation.push(destinationStyle: style,
window: window,
content)
}
public func push<C: GraniteNavigationDestination>(window: GraniteRouteWindowProperties? = nil,
@ViewBuilder _ content: @escaping () -> C) {
self.navigation.push(window: window,
content)
}
public func pop() {
self.navigation.pop()
}
}
...
}This is essentially the main router that is exposed to your application's Views as EnvironmentValues. Allowing you to programmatically push and pop views freely wherever available.
struct PostCardView: View {
@Environment(\.graniteRouter) var router
...
var body: some View {
Button {
router.push {
PostDisplayView(context: _context,
updatedModel: currentModel)
}
} label: {
Text("Push")
}
SomeView()
.route {
PostDisplayView(context: _context,
updatedModel: currentModel)
} with: { router }
}
}As simple as that. Multiple view extensions are available to easily access route functionality or you can simply call push or pop directly to execute. And the captured View or GraniteComponent will slide on top of the caller.
self.navigation.push(window: window, content)The Router struct finds its representing GraniteNavigation instance to use their push/pop functions.
public extension GraniteNavigation {
func push<Component: View>(destinationStyle style: GraniteNavigationDestinationStyle = .init(),
window: GraniteRouteWindowProperties? = nil,
@ViewBuilder _ component: @escaping (() -> Component)) {
let component = NavigationComponent<Component>(component)
let memadd = self.set(destinationStyle: style) {
component
}
self.push(memadd, window: window)
}
}The extension itself is seen wrapping the component in a NavigationComponent, setting the component. Returning a heap address to target the push when ready.
public func routeButton<C: GraniteNavigationDestination>(title: String = "",
window: GraniteRouteWindowProperties? = nil,
@ViewBuilder component : @escaping (() -> C),
with router: @escaping (() -> GraniteNavigation.Router)) -> some View {
let router = router()
let memadd = router.navigation.set {
NavigationComponent<C>(component)
}
return Button {
#if os(iOS)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
#endif
router.navigation.push(memadd, window: window)
} label: {
self
}
}set is separate from push for cases where we'd like for a condition to occur prior to pushing. But, we'd like to set the component for optimal presentation time.
The view extensions like above simply allow for cleaner conformance within a var body: some View builder in a standard View. In theory you can simply use the router directly for an entire app.
An actual snippet of the set function in GraniteNavigation being called above.
func set<Component : GraniteComponent>(destinationStyle: GraniteNavigationDestinationStyle = .init(),
@ViewBuilder _ component: @escaping (() -> Component)) -> String {
let screen = NavigationPassthroughComponent<Component, EmptyGranitePayload>.Screen<Component, EmptyGranitePayload>.init(component)
let addr = NSString(format: "%p", addressHeap(o: screen)) as String
paths[addr] = { AnyView(NavigationPassthroughComponent<Component,
EmptyGranitePayload>(screen: screen)
.environment(\.graniteNavigationDestinationStyle, destinationStyle)) }
isActive[addr] = false
return addr
}The screen is important for setting GraniteNavigationDestinationStyle where presenting views, on your end, can customize trailing navigation bar items and other properties. It also helps with managing the lifecycle of the presentation and animating the "segues".