Skip to content

Instantly share code, notes, and snippets.

@jerrypm
Last active September 16, 2025 05:32
Show Gist options
  • Select an option

  • Save jerrypm/e212a7d83d9095344e3c514ee71e17be to your computer and use it in GitHub Desktop.

Select an option

Save jerrypm/e212a7d83d9095344e3c514ee71e17be to your computer and use it in GitHub Desktop.
import SwiftUI
import Combine
import UIKit
import SafariServices
// MARK: - Route Protocols
/// Protocol defining a navigation route that can be executed from a view controller
///
/// Routes encapsulate navigation logic and can be executed from any UIViewController.
/// This allows for decoupled navigation where views don't need to know about specific destinations.
protocol Route {
/// Executes the navigation route from the specified source view controller
/// - Parameter source: The view controller that will initiate the navigation
func execute(from source: UIViewController)
}
/// A no-op route that performs no navigation action
///
/// Used as a fallback when no specific route handler is found
/// or when you need to represent the absence of navigation.
struct EmptyRoute: Route {
/// Performs no action when executed
/// - Parameter source: The source view controller (unused)
func execute(from source: UIViewController) { }
}
/// Protocol for UIKit-specific routes that support animation
///
/// Extends the base Route protocol to include animation configuration
/// for UIKit navigation transitions.
protocol UIKitRoute: Route {
/// Whether the navigation should be animated
var isAnimated: Bool { get }
}
/// A route that presents a view controller modally
///
/// Creates and presents a new view controller using the modal presentation style.
/// The destination is created lazily when the route is executed.
struct ModalRoute: UIKitRoute {
/// Factory closure that creates the destination view controller
let destination: () -> UIViewController
/// Whether the modal presentation should be animated
let isAnimated: Bool
/// Presents the destination view controller modally from the source
/// - Parameter source: The view controller that will present the modal
func execute(from source: UIViewController) {
let vc = destination()
source.present(vc, animated: isAnimated)
}
}
/// A route that pushes a view controller onto the navigation stack
///
/// Creates and pushes a new view controller onto the source's navigation controller.
/// Requires the source to be embedded in a navigation controller.
struct PushRoute: UIKitRoute {
/// Factory closure that creates the destination view controller
let destination: () -> UIViewController
/// Whether the push transition should be animated
let isAnimated: Bool
/// Pushes the destination view controller onto the navigation stack
/// - Parameter source: The view controller whose navigation controller will perform the push
/// - Note: Does nothing if source is not embedded in a navigation controller
func execute(from source: UIViewController) {
guard let nav = source.navigationController else { return }
let vc = destination()
nav.pushViewController(vc, animated: isAnimated)
}
}
/// A route that navigates back in the navigation hierarchy
///
/// Intelligently determines whether to pop from navigation stack or dismiss modally
/// based on the current navigation context.
struct BackRoute: UIKitRoute {
/// Whether the back navigation should be animated
let isAnimated: Bool
/// Navigates back by either popping from navigation stack or dismissing modally
/// - Parameter source: The view controller to navigate back from
/// - Note: Pops if in navigation controller with multiple view controllers, otherwise dismisses
func execute(from source: UIViewController) {
if let nav = source.navigationController, nav.viewControllers.count > 1 {
nav.popViewController(animated: isAnimated)
} else {
source.dismiss(animated: isAnimated)
}
}
}
/// A route that presents an alert dialog
///
/// Creates and presents a simple alert with a title, message, and OK button.
/// Useful for displaying error messages or simple notifications.
struct AlertRoute: UIKitRoute {
/// The title text for the alert
let title: String
/// The message text for the alert
let message: String
/// Whether the alert presentation should be animated
let isAnimated: Bool
/// Presents an alert controller with the configured title and message
/// - Parameter source: The view controller that will present the alert
func execute(from source: UIViewController) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
source.present(alert, animated: isAnimated)
}
}
/// A route that opens a URL either in-app or in the external browser
///
/// Provides flexibility to open URLs using SFSafariViewController for in-app browsing
/// or the system browser for external navigation.
struct BrowserRoute: UIKitRoute {
/// The URL to open
let url: URL
/// Whether the presentation should be animated (only applies to in-app browsing)
let isAnimated: Bool
/// Whether to open the URL in the external browser instead of in-app
let openExternally: Bool
/// Opens the URL either in-app using SFSafariViewController or externally
/// - Parameter source: The view controller that will present the Safari view controller (if opening in-app)
func execute(from source: UIViewController) {
if openExternally {
UIApplication.shared.open(url)
} else {
let safariVC = SFSafariViewController(url: url)
source.present(safariVC, animated: isAnimated)
}
}
}
// MARK: - Route Definitions
/// Protocol for route definitions that can be compared for equality
///
/// Route definitions are value types that describe navigation destinations
/// without containing the actual navigation logic. They are used by the router
/// to determine which route handler to execute.
protocol RouteDefinition: Equatable { }
/// Route definition for navigating to a talk detail screen
///
/// Contains the talk data needed to display the detail view.
/// Two route definitions are considered equal if they reference the same talk.
struct TalkDetailRouteDefinition: RouteDefinition {
/// The talk to display in the detail view
let talk: Talk
/// Compares two route definitions for equality based on talk ID
/// - Parameters:
/// - lhs: Left-hand side route definition
/// - rhs: Right-hand side route definition
/// - Returns: True if both definitions reference the same talk
static func == (lhs: TalkDetailRouteDefinition, rhs: TalkDetailRouteDefinition) -> Bool {
lhs.talk.id == rhs.talk.id
}
}
/// Route definition for opening a URL in a browser
///
/// Contains the URL to be opened. Two route definitions are considered
/// equal if they reference the same URL.
struct BrowserRouteDefinition: RouteDefinition {
/// The URL to open in the browser
let url: URL
/// Compares two route definitions for equality based on URL
/// - Parameters:
/// - lhs: Left-hand side route definition
/// - rhs: Right-hand side route definition
/// - Returns: True if both definitions reference the same URL
static func == (lhs: BrowserRouteDefinition, rhs: BrowserRouteDefinition) -> Bool {
lhs.url == rhs.url
}
}
/// Route definition for navigating back in the navigation hierarchy
///
/// A stateless route definition that represents a back navigation action.
/// All instances are considered equal since they perform the same action.
struct BackRouteDefinition: RouteDefinition {
/// All back route definitions are considered equal
/// - Parameters:
/// - lhs: Left-hand side route definition
/// - rhs: Right-hand side route definition
/// - Returns: Always true since all back routes are equivalent
static func == (lhs: BackRouteDefinition, rhs: BackRouteDefinition) -> Bool { true }
}
/// Route definition for displaying an alert dialog
///
/// Contains the title and message for the alert. Two route definitions
/// are considered equal if they have the same title and message.
struct AlertRouteDefinition: RouteDefinition {
/// The title text for the alert
let title: String
/// The message text for the alert
let message: String
/// Compares two route definitions for equality based on title and message
/// - Parameters:
/// - lhs: Left-hand side route definition
/// - rhs: Right-hand side route definition
/// - Returns: True if both definitions have the same title and message
static func == (lhs: AlertRouteDefinition, rhs: AlertRouteDefinition) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message
}
}
// MARK: - Router
/// Central router that handles navigation throughout the application
///
/// The router decouples navigation logic from view controllers by using a publish-subscribe pattern.
/// View models send route definitions, and view controllers subscribe to execute the actual navigation.
class Router {
/// Internal subject for publishing route definitions
private let subject = PassthroughSubject<any RouteDefinition, Never>()
/// Container that resolves route definitions to concrete route implementations
private let routeContainer: RouteContainer
/// Creates a new router with the specified route container
/// - Parameter routeContainer: The container used to resolve route definitions
init(routeContainer: RouteContainer) {
self.routeContainer = routeContainer
}
/// Sends a route definition for execution
/// - Parameter route: The route definition to execute
func send(_ route: any RouteDefinition) {
subject.send(route)
}
/// Exposes the route publisher for testing without leaking the internal subject
/// - Returns: A publisher that emits route definitions
var publisher: AnyPublisher<any RouteDefinition, Never> {
subject.eraseToAnyPublisher()
}
/// Subscribes a view controller to execute routes sent through this router
/// - Parameter viewController: The view controller that will execute the routes
/// - Returns: A cancellable subscription that should be stored to maintain the subscription
func subscribe(from viewController: UIViewController) -> AnyCancellable {
subject
.compactMap { [weak routeContainer] def in
routeContainer?.resolve(def)
}
.receive(on: DispatchQueue.main)
.sink { route in
route.execute(from: viewController)
}
}
}
// MARK: - Route Container
/// Dependency injection container for route handlers
///
/// Maps route definition types to their corresponding route implementations.
/// Allows for flexible configuration of navigation behavior throughout the app.
class RouteContainer {
/// Dictionary mapping type names to route factory functions
private var registrations: [String: (any RouteDefinition) -> Route] = [:]
/// Registers a route handler for a specific route definition type
/// - Parameters:
/// - type: The route definition type to register a handler for
/// - handler: A closure that creates a route from the definition
func register<T: RouteDefinition>(_ type: T.Type, handler: @escaping (T) -> Route) {
registrations[String(describing: T.self)] = { def in
guard let typed = def as? T else { return EmptyRoute() }
return handler(typed)
}
}
/// Resolves a route definition to its corresponding route implementation
/// - Parameter definition: The route definition to resolve
/// - Returns: The route implementation, or EmptyRoute if no handler is registered
func resolve(_ definition: any RouteDefinition) -> Route {
registrations[String(describing: type(of: definition))]?(definition) ?? EmptyRoute()
}
}
// MARK: - Model
/// Represents a talk or presentation
///
/// A simple model object that conforms to Identifiable for use in SwiftUI lists.
/// Contains the basic information needed to display and navigate to talk details.
struct Talk: Identifiable {
/// Unique identifier for the talk
let id: String
/// Display title of the talk
let title: String
}
// MARK: - View Models
/// View model for the talk list screen
///
/// Manages the list of talks and handles user interactions like navigation to detail screens,
/// opening external links, and displaying error messages.
class TalkListViewModel: ObservableObject {
/// Router for handling navigation actions
let router: Router
/// Published array of talks displayed in the list
@Published var talks: [Talk]
/// Creates a new talk list view model
/// - Parameters:
/// - router: The router to use for navigation
/// - talks: The initial list of talks to display
init(router: Router, talks: [Talk]) {
self.router = router
self.talks = talks
}
/// Navigates to the detail screen for the specified talk
/// - Parameter talk: The talk to view in detail
func open(_ talk: Talk) {
router.send(TalkDetailRouteDefinition(talk: talk))
}
/// Opens the Swift Heroes website in a browser
func browse() {
let url = URL(string: "https://swiftheroes.com")!
router.send(BrowserRouteDefinition(url: url))
}
/// Displays an error alert to the user
func showError() {
router.send(AlertRouteDefinition(title: "Error", message: "Something went wrong."))
}
}
/// View model for the talk detail screen
///
/// Manages the display of a single talk's details and handles back navigation.
class TalkDetailViewModel: ObservableObject {
/// The talk being displayed
let talk: Talk
/// Router for handling navigation actions
let router: Router
/// Creates a new talk detail view model
/// - Parameters:
/// - talk: The talk to display
/// - router: The router to use for navigation
init(talk: Talk, router: Router) {
self.talk = talk
self.router = router
}
/// Navigates back to the previous screen
func goBack() {
router.send(BackRouteDefinition())
}
}
// MARK: - SwiftUI Views
/// SwiftUI view displaying a list of talks
///
/// Presents talks in a list format with toolbar buttons for additional actions.
/// Tapping a talk navigates to its detail screen.
struct TalkListView: View {
/// View model managing the talk list state and actions
@StateObject private var viewModel: TalkListViewModel
/// Creates a new talk list view with the specified view model
/// - Parameter viewModel: The view model to use for this view
init(viewModel: TalkListViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
List(viewModel.talks) { talk in
Text(talk.title)
.onTapGesture {
viewModel.open(talk)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button(action: { viewModel.browse() }) {
Image(systemName: "safari")
}
Button(action: { viewModel.showError() }) {
Image(systemName: "exclamationmark.triangle")
}
}
}
}
}
}
/// SwiftUI view displaying the details of a single talk
///
/// Shows the talk title and provides a button to navigate back to the previous screen.
struct TalkDetailView: View {
/// View model managing the talk detail state and actions
@StateObject private var viewModel: TalkDetailViewModel
/// Creates a new talk detail view with the specified view model
/// - Parameter viewModel: The view model to use for this view
init(viewModel: TalkDetailViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
Text(viewModel.talk.title)
.font(.title)
Button("Go Back") {
viewModel.goBack()
}
.padding()
Spacer()
}
.padding()
}
}
// MARK: - UIKit Hosting Controllers
/// UIKit view controller that hosts the SwiftUI TalkListView
///
/// Bridges SwiftUI views with UIKit navigation by subscribing to router events
/// and handling navigation actions through the UIKit presentation APIs.
class TalkListViewController: UIHostingController<TalkListView> {
/// Set of cancellables for managing Combine subscriptions
private var cancellables = Set<AnyCancellable>()
/// View model for the hosted SwiftUI view
private let viewModel: TalkListViewModel
/// Creates a new hosting controller with the specified view model
/// - Parameter viewModel: The view model to use for the SwiftUI view
init(viewModel: TalkListViewModel) {
self.viewModel = viewModel
super.init(rootView: TalkListView(viewModel: viewModel))
}
/// Initializer from storyboard - not supported
/// - Parameter coder: The unarchiver object
/// - Important: This initializer is unavailable and will cause a fatal error
@MainActor required dynamic init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called after the controller's view is loaded into memory
///
/// Sets up the router subscription to handle navigation events from the SwiftUI view.
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.router
.subscribe(from: self)
.store(in: &cancellables)
}
}
/// UIKit view controller that hosts the SwiftUI TalkDetailView
///
/// Bridges SwiftUI views with UIKit navigation by subscribing to router events
/// and handling navigation actions through the UIKit presentation APIs.
class TalkDetailViewController: UIHostingController<TalkDetailView> {
/// Set of cancellables for managing Combine subscriptions
private var cancellables = Set<AnyCancellable>()
/// View model for the hosted SwiftUI view
private let viewModel: TalkDetailViewModel
/// Creates a new hosting controller with the specified view model
/// - Parameter viewModel: The view model to use for the SwiftUI view
init(viewModel: TalkDetailViewModel) {
self.viewModel = viewModel
super.init(rootView: TalkDetailView(viewModel: viewModel))
}
/// Initializer from storyboard - not supported
/// - Parameter coder: The unarchiver object
/// - Important: This initializer is unavailable and will cause a fatal error
@MainActor required dynamic init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called after the controller's view is loaded into memory
///
/// Sets up the router subscription to handle navigation events from the SwiftUI view.
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.router
.subscribe(from: self)
.store(in: &cancellables)
}
}
// MARK: - App Setup
/// Main application entry point demonstrating SwiftUI and UIKit navigation integration
///
/// This app showcases a router-based navigation system that allows SwiftUI views
/// to trigger UIKit navigation actions through a decoupled routing architecture.
//@main
//struct NavigationExampleApp: App {
// var body: some Scene {
// WindowGroup {
// EmptyView()
// .onAppear { setupApp() }
// }
// }
//
// /// Configures the application's dependency injection and navigation setup
// ///
// /// Sets up the route container with handlers for all route types, creates the initial
// /// view hierarchy, and configures the root window with a navigation controller.
// private func setupApp() {
// let container = RouteContainer()
//
// // Register route handlers for each route definition type
//
// /// Handler for talk detail navigation - presents modally
// container.register(TalkDetailRouteDefinition.self) { def in
// ModalRoute(destination: {
// let router = Router(routeContainer: container)
// let vm = TalkDetailViewModel(talk: def.talk, router: router)
// return TalkDetailViewController(viewModel: vm)
// }, isAnimated: true)
// }
//
// /// Handler for browser navigation - opens in-app Safari view
// container.register(BrowserRouteDefinition.self) { def in
// BrowserRoute(url: def.url, isAnimated: true, openExternally: false)
// }
//
// /// Handler for back navigation - pops or dismisses based on context
// container.register(BackRouteDefinition.self) { _ in
// BackRoute(isAnimated: true)
// }
//
// /// Handler for alert display - shows system alert
// container.register(AlertRouteDefinition.self) { def in
// AlertRoute(title: def.title, message: def.message, isAnimated: true)
// }
//
// // Create the initial app state and view hierarchy
// let router = Router(routeContainer: container)
// let talks = [
// Talk(id: "1", title: "SwiftUI Navigation"),
// Talk(id: "2", title: "UIKit Integration")
// ]
// let vm = TalkListViewModel(router: router, talks: talks)
// let rootVC = TalkListViewController(viewModel: vm)
//
// // Configure the root window with navigation controller
// if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
// let window = UIWindow(windowScene: scene)
// window.rootViewController = UINavigationController(rootViewController: rootVC)
// window.makeKeyAndVisible()
// }
// }
//}
/// If you want to use UIKit - AppDelegate use this ↓↓↓↓↓↓↓↓↓
class NavigationSetup {
static func configure() -> UIViewController {
let container = RouteContainer()
// Register route handlers
container.register(TalkDetailRouteDefinition.self) { def in
ModalRoute(destination: {
let router = Router(routeContainer: container)
let vm = TalkDetailViewModel(talk: def.talk, router: router)
return TalkDetailViewController(viewModel: vm)
}, isAnimated: true)
}
/// Handler for browser navigation - opens in-app Safari view
container.register(BrowserRouteDefinition.self) { def in
BrowserRoute(url: def.url, isAnimated: true, openExternally: false)
}
/// Handler for back navigation - pops or dismisses based on context
container.register(BackRouteDefinition.self) { _ in
BackRoute(isAnimated: true)
}
/// Handler for alert display - shows system alert
container.register(AlertRouteDefinition.self) { def in
AlertRoute(title: def.title, message: def.message, isAnimated: true)
}
let router = Router(routeContainer: container)
let talks = [
Talk(id: "1", title: "SwiftUI Navigation"),
Talk(id: "2", title: "UIKit Integration")
]
let vm = TalkListViewModel(router: router, talks: talks)
return TalkListViewController(viewModel: vm)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment