Instantly share code, notes, and snippets.
Last active
September 16, 2025 05:32
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save jerrypm/e212a7d83d9095344e3c514ee71e17be to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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