Granite (SwiftUI)

GraniteNavigation.Router

Swift
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.

Swift
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.

Swift
self.navigation.push(window: window, content)

The Router struct finds its representing GraniteNavigation instance to use their push/pop functions.

Swift
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.

Swift
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.

Swift
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".