├── .gitattributes ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Tests └── MSwiftUINavigatorTests │ └── MSwiftUINavigatorTests.swift ├── Sources └── MSwiftUINavigator │ ├── RootApp.swift │ ├── View+OnBackSwipe.swift │ ├── Navigator+Dialog.swift │ ├── Navigator+ActionSheet.swift │ ├── UIApplication+TopVewController.swift │ └── NavigationManager.swift ├── LICENSE ├── Package.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "fittedsheets", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/gordontucker/FittedSheets.git", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "3dcebb20ff42f644e9474a198acb886d4bd51793" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Tests/MSwiftUINavigatorTests/MSwiftUINavigatorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MSwiftUINavigator 3 | 4 | final class MSwiftUINavigatorTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/RootApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootApp.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct RootApp: View { 12 | 13 | private let view: V 14 | 15 | public init(view: V) { 16 | self.view = view 17 | } 18 | 19 | public var body: some View { 20 | view 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/View+OnBackSwipe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+OnBackSwipe.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension View { 12 | 13 | func onBackSwipe(perform action: @escaping () -> Void) -> some View { 14 | gesture( 15 | DragGesture() 16 | .onEnded({ value in 17 | if value.startLocation.x < 50 && value.translation.width > 80 { 18 | action() 19 | } 20 | }) 21 | ) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/Navigator+Dialog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigator+Dialog.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct NavigatorCustomDialog: ViewModifier { 12 | private let dialogContent: () -> DialogContent 13 | 14 | public init( 15 | dialogContent: @escaping () -> DialogContent) { 16 | self.dialogContent = dialogContent 17 | } 18 | 19 | public func body(content: Content) -> some View { 20 | ZStack { 21 | content 22 | Rectangle().foregroundColor(Color.black.opacity(0.6)) 23 | ZStack { 24 | dialogContent() 25 | } 26 | .padding(20) 27 | } 28 | } 29 | } 30 | 31 | internal extension View { 32 | func presentAsNavigatorDialog( 33 | @ViewBuilder dialogContent: @escaping () -> DialogContent 34 | ) -> some View { 35 | modifier(NavigatorCustomDialog(dialogContent: dialogContent)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mahmoud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MSwiftUINavigator", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "MSwiftUINavigator", 13 | targets: ["MSwiftUINavigator"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/gordontucker/FittedSheets.git", branch: "main") 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "MSwiftUINavigator", 23 | dependencies: ["FittedSheets"]), 24 | .testTarget( 25 | name: "MSwiftUINavigatorTests", 26 | dependencies: ["MSwiftUINavigator"]), 27 | ], 28 | swiftLanguageVersions: [.v5] 29 | ) 30 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/Navigator+ActionSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigator+ActionSheet.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct NavigatorCustomActionSheet: ViewModifier { 12 | private let actionSheetContent: () -> ActionSheet 13 | @State private var showActionSheet = true 14 | 15 | public init( 16 | actionSheetContent: @escaping () -> ActionSheet) { 17 | self.actionSheetContent = actionSheetContent 18 | } 19 | 20 | public func body(content: Content) -> some View { 21 | ZStack { 22 | content 23 | Rectangle().foregroundColor(Color.black.opacity(0.6)) 24 | }.actionSheet(isPresented: $showActionSheet) { 25 | actionSheetContent() 26 | } 27 | } 28 | } 29 | 30 | internal extension View { 31 | func presentAsNavigatorActionSheet( 32 | @ViewBuilder actionSheetContent: @escaping () -> ActionSheet 33 | ) -> some View { 34 | modifier(NavigatorCustomActionSheet(actionSheetContent: actionSheetContent)) 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/UIApplication+TopVewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+TopVewController.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIApplication { 12 | public func topViewController() -> UIViewController? { 13 | let window = UIApplication.shared.keyWindowScene 14 | return topViewControllerRecursive(controller: window.rootViewController) 15 | } 16 | 17 | func topViewControllerRecursive(controller: UIViewController?) -> UIViewController? { 18 | if let navigationController = controller as? UINavigationController { 19 | return topViewControllerRecursive(controller: navigationController.visibleViewController) 20 | } 21 | if let tabController = controller as? UITabBarController, 22 | let selected = tabController.selectedViewController { 23 | return topViewControllerRecursive(controller: selected) 24 | } 25 | if let presented = controller?.presentedViewController { 26 | return topViewControllerRecursive(controller: presented) 27 | } 28 | return controller 29 | } 30 | 31 | 32 | public var keyWindowScene: UIWindow { 33 | // Get connected scenes 34 | return UIApplication.shared.connectedScenes 35 | // Keep only active scenes, onscreen and visible to the user 36 | .filter { $0.activationState == .foregroundActive } 37 | // Keep only the first `UIWindowScene` 38 | .first(where: { $0 is UIWindowScene }) 39 | // Get its associated windows 40 | .flatMap({ $0 as? UIWindowScene })?.windows 41 | // Finally, keep only the key window 42 | .first(where: \.isKeyWindow) ?? UIApplication.shared.connectedScenes 43 | // Keep only Inactive scenes, onscreen and visible to the user 44 | .filter { $0.activationState == .foregroundInactive } 45 | // Keep only the first `UIWindowScene` 46 | .first(where: { $0 is UIWindowScene }) 47 | // Get its associated windows 48 | .flatMap({ $0 as? UIWindowScene })?.windows 49 | // Finally, keep only the key window 50 | .first(where: \.isKeyWindow) ?? UIWindow() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # MSwiftUINavigator 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | 12 | MSwiftUINavigator is a Swift package that provides a navigation solution for SwiftUI applications, leveraging the UIKit navigation system. It simplifies common navigation tasks and integrates seamlessly with SwiftUI views. 13 | 14 | ## Features 15 | 16 | - Push views onto the navigation stack. 17 | - Present views modally with custom transition and presentation styles. 18 | - Dismiss views and navigate back. 19 | - Pop to the root view. 20 | - Present sheets with customizable sizes using the FittedSheets library. 21 | - Present dialogs and action sheets above the current view. 22 | - Pop to a specific view type in the navigation stack. 23 | - Present dialogs and action sheets above the current view, with options for customizations. 24 | 25 | ## Installation 26 | 27 | You can add MSwiftUINavigator to your Swift package by adding it as a dependency in your `Package.swift` file: 28 | 29 | ```swift 30 | dependencies: [ 31 | .package(url: "https://github.com/MahmoudAbdelshafi/MSwiftUINavigator.git", .branch("main")) 32 | ], 33 | ``` 34 | 35 | ## Usage 36 | 37 | To use MSwiftUINavigator in your SwiftUI project, you'll need to import it and conform to the Navigator protocol in the main view where you want to use the navigator: 38 | 39 | ```swift 40 | import MSwiftUINavigator 41 | 42 | struct ContentView: View, Navigator { 43 | // Your view code here 44 | } 45 | ``` 46 | Additionally, you can access the Navigator as an @Environment object: 47 | 48 | ```swift 49 | @Environment(\.navigator) var navigator 50 | 51 | navigator.presentSheet { 52 | // Your view code here 53 | } 54 | 55 | navigator.pushView { 56 | // Your view code here 57 | } 58 | 59 | ``` 60 | 61 | To present an action sheet, you can use the `presentActionSheet` function provided by MSwiftUINavigator: 62 | 63 | ```swift 64 | navigator.presentActionSheet { 65 | ActionSheet( 66 | title: Text("Choose an action"), 67 | message: Text("What would you like to do?"), 68 | buttons: [ 69 | .default(Text("Option 1")) { 70 | // Handle option 1 71 | }, 72 | .default(Text("Option 2")) { 73 | // Handle option 2 74 | }, 75 | .cancel() 76 | ] 77 | ) 78 | } 79 | ``` 80 | 81 | For singleton access to the NavigationManager, you can use the shared instance like this: 82 | 83 | ```swift 84 | 85 | NavigationManager.shared.presentView(transitionStyle: .coverVertical, 86 | presentStyle: .fullScreen, 87 | animated: true) { 88 | // Your View here 89 | } 90 | 91 | ``` 92 | 93 | There's also an example project available on GitHub for reference: [MSwiftUINavigatorExample](https://github.com/MahmoudAbdelshafi/MSwiftUINavigatorExample). 94 | 95 | ## Dependencies 96 | 97 | MSwiftUINavigator relies on the following external dependency to enhance its functionality, particularly for handling popups, sheets, and dialogs: 98 | 99 | - **FittedSheets**: FittedSheets is a powerful library available on [GitHub](https://github.com/gordontucker/FittedSheets) that provides advanced capabilities for presenting sheets with customizable sizes and behaviors. MSwiftUINavigator utilizes FittedSheets to create dynamic and interactive sheet presentations, enhancing the user experience when displaying popups, sheets, and dialogs in your SwiftUI applications. 100 | 101 | ## License 102 | 103 | This package is released under the MIT License. 104 | 105 | ## Author 106 | 107 | - **Mahmoud Abdelshafi** 108 | - [GitHub](https://github.com/MahmoudAbdelshafi) 109 | - [LinkedIn](https://www.linkedin.com/in/mahmoud-abd-el-shafi/) 110 | 111 | 112 | - **Soliman El Far** 113 | - [GitHub](https://github.com/aoliman) 114 | - [LinkedIn](https://www.linkedin.com/in/soliman-yousry-74050a155/) 115 | -------------------------------------------------------------------------------- /Sources/MSwiftUINavigator/NavigationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationManager.swift 3 | // MNavigator 4 | // 5 | // Created by Soliman Elfar and Mahmoud Abdelshafion 01/09/2023. 6 | // All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import UIKit 12 | import FittedSheets 13 | 14 | public protocol Navigator: View { 15 | // In a view where you want to use a navigator, the view must implement [AppNavigator]. 16 | // It should be implemented in the main screen. 17 | } 18 | 19 | // MARK: - Shared value- 20 | 21 | public extension Navigator { 22 | /// The shared instance of the `NavigationManager`. 23 | var navigator: NavigationManager { 24 | return NavigationManager.shared 25 | } 26 | } 27 | 28 | public extension EnvironmentValues { 29 | /// The shared instance of the `NavigationManager`. 30 | var navigator: NavigationManager { 31 | return NavigationManager.shared 32 | } 33 | } 34 | 35 | // MARK: - Enum PopPositionType - 36 | 37 | public enum PopPositionType { 38 | case first, last 39 | } 40 | 41 | // MARK: - NavigationManager - 42 | 43 | /// This Swift file defines a navigation solution for SwiftUI applications, leveraging the UIKit navigation system. It provides a set of functions and extensions under the `NavigationManager` struct to facilitate common navigation tasks such as presenting views, pushing views, handling sheets, dialogs, and action sheets, as well as managing the app's navigation stack. 44 | 45 | /// The `NavigationManager` struct is designed to simplify and streamline the navigation logic within SwiftUI views, making it easier to navigate between different screens, present modals, and manage the navigation stack. It also includes utility functions for finding and retrieving the current navigation controller within the app's view controller hierarchy. 46 | 47 | /// This navigation solution integrates seamlessly with SwiftUI views and can be used to create sophisticated navigation flows in your SwiftUI-based iOS applications while harnessing the power of UIKit's navigation capabilities. 48 | 49 | /// Note: This file assumes that UIKit is used for navigation, and it provides a bridge between SwiftUI and UIKit for handling navigation tasks effectively. 50 | 51 | public struct NavigationManager { 52 | /// The shared instance of the `NavigationManager`. 53 | public static let shared = NavigationManager() 54 | 55 | private init() {} 56 | 57 | /// Reset the root window of the app with a new view. 58 | /// 59 | /// - Parameters: 60 | /// - rootView: A closure that returns the root view. 61 | public func resetRootWindow(rootView: () -> T) { 62 | let window = UIApplication.shared.keyWindowScene 63 | window.isHidden = false 64 | let hostingVC = UIHostingController(rootView: RootApp(view: rootView())) 65 | let mainNavVC = UINavigationController(rootViewController: hostingVC) 66 | mainNavVC.navigationBar.isHidden = true 67 | window.rootViewController = mainNavVC 68 | window.makeKeyAndVisible() 69 | } 70 | 71 | /// Push a new view above the current view. 72 | /// 73 | /// - Parameters: 74 | /// - view: A closure that returns the view to push. 75 | /// - animated: Optional. Whether to animate the transition. Default is true. 76 | public func pushView(view: () -> T, 77 | animated: Bool? = nil) { 78 | let nav = NavigationManager.getCurrentNavigationController() 79 | let swipView = view().onBackSwipe { 80 | dismiss() 81 | } 82 | nav?.pushViewController(UIHostingController(rootView: swipView), animated: animated ?? true) 83 | } 84 | 85 | /// Present a new view above the current view with custom transition and presentation styles. 86 | /// 87 | /// - Parameters: 88 | /// - transitionStyle: Optional. The transition style. Default is .coverVertical. 89 | /// - presentStyle: Optional. The presentation style. Default is .fullScreen. 90 | /// - animated: Optional. Whether to animate the presentation. Default is true. 91 | /// - view: A closure that returns the view to present. 92 | public func presentView(transitionStyle: UIModalTransitionStyle? = nil, 93 | presentStyle: UIModalPresentationStyle? = nil, 94 | animated: Bool? = nil, view: () -> T) { 95 | guard let topViewController = UIApplication.shared.topViewController() 96 | else { 97 | return 98 | } 99 | topViewController.modalTransitionStyle = transitionStyle ?? .coverVertical 100 | topViewController.modalPresentationStyle = presentStyle ?? .fullScreen 101 | topViewController.present(UIHostingController(rootView: view()), animated: animated ?? true) 102 | } 103 | 104 | /// Dismiss the current view. 105 | /// 106 | /// - Parameters: 107 | /// - animated: Optional. Whether to animate the dismissal. Default is true. 108 | /// - completion: Optional. A closure to be called upon completion of dismissal. 109 | public func dismiss(animated: Bool? = nil, 110 | completion: (() -> Void)? = nil) { 111 | guard let topViewController = UIApplication.shared.topViewController() 112 | else { 113 | return 114 | } 115 | guard let navigation = topViewController.navigationController else { 116 | topViewController.dismiss(animated: animated ?? true, 117 | completion: completion) 118 | return 119 | } 120 | navigation.popViewController(animated: animated ?? true) 121 | completion?() 122 | } 123 | 124 | /// Pops (removes) the top view controller from the current navigation stack. 125 | /// 126 | /// - Parameters: 127 | /// - animated: Optional. Whether to animate the pop transition. Default is true. 128 | /// - completion: Optional. A closure to be called upon completion of the pop transition. 129 | public func popView(animated: Bool? = nil, 130 | completion: (() -> Void)? = nil) { 131 | // Ensure there is a valid navigation controller 132 | guard let topNavigation = NavigationManager.getCurrentNavigationController() 133 | else { 134 | return 135 | } 136 | 137 | // Pop the top view controller from the navigation stack 138 | topNavigation.popViewController(animated: animated ?? true) 139 | 140 | // Execute the completion closure, if provided 141 | completion?() 142 | } 143 | 144 | /// Pop to the root view. 145 | /// 146 | /// - Parameters: 147 | /// - animated: Optional. Whether to animate the transition. Default is true. 148 | public func popToRootView(animated: Bool? = nil) { 149 | let nav = NavigationManager.getCurrentNavigationController() 150 | nav?.popToRootViewController(animated: animated ?? true) 151 | } 152 | 153 | /// Present a sheet above the current view with customizable sizes using the FittedSheets library. 154 | /// 155 | /// - Parameters: 156 | /// - sizes: Optional. An array of `SheetSize` options. Default is [.intrinsic]. 157 | /// - view: A closure that returns the view to present as a sheet. 158 | /// 159 | /// Note: This function utilizes the FittedSheets library available at: https://github.com/gordontucker/FittedSheets 160 | /// 161 | /// Example usage: 162 | /// 163 | /// ```swift 164 | /// navigator.presentSheet(sizes: [.fixed(300)], view: { 165 | /// CustomSheetContentView() 166 | /// }) 167 | /// ``` 168 | /// 169 | /// - Important: Make sure to include the FittedSheets library in your project for this function to work. 170 | public func presentSheet(sizes: [SheetSize] = [.intrinsic], 171 | view: () -> T) { 172 | let appHostingController = UIHostingController(rootView: view()) 173 | appHostingController.view.backgroundColor = UIColor.clear 174 | 175 | var options = SheetOptions() 176 | options.pullBarHeight = 0 177 | options.shouldExtendBackground = false 178 | options.useFullScreenMode = false 179 | options.shrinkPresentingViewController = false 180 | 181 | let sheet = SheetViewController(controller: appHostingController, 182 | sizes: sizes, options: options) 183 | sheet.treatPullBarAsClear = true 184 | sheet.overlayColor = UIColor.black.withAlphaComponent(0.2) 185 | sheet.minimumSpaceAbovePullBar = 1 186 | sheet.cornerRadius = 30 187 | sheet.gripSize = CGSize(width: 142, height: 0) 188 | sheet.gripColor = UIColor.clear 189 | let window = UIApplication.shared.windows.first 190 | /// adjust bottom pading from safearea 191 | let bottomPadding = window?.safeAreaInsets.bottom 192 | sheet.additionalSafeAreaInsets = UIEdgeInsets(top: 0, 193 | left: 0, 194 | bottom: -(bottomPadding ?? 0), right: 0) 195 | 196 | sheet.contentViewController.contentBackgroundColor = .clear 197 | sheet.contentViewController.childViewController.view.backgroundColor = .clear 198 | sheet.contentViewController.view.backgroundColor = .clear 199 | 200 | let nav = NavigationManager.getCurrentNavigationController() 201 | nav?.present(sheet, animated: true, completion: nil) 202 | } 203 | 204 | /// Dismiss the currently presented sheet. 205 | /// 206 | /// - Parameters: 207 | /// - animated: Optional. Whether to animate the dismissal. Default is true. 208 | /// - completion: Optional. A closure to be called upon completion of dismissal. 209 | public func dismissSheet(animated: Bool? = nil, 210 | completion: (() -> Void)? = nil) { 211 | dismiss() 212 | } 213 | 214 | /// Present a dialog above the current view. 215 | /// 216 | /// - Parameters: 217 | /// - view: A closure that returns the view to present as a dialog. 218 | /// - animated: Optional. Whether to animate the presentation. Default is false. 219 | public func presentDialog(view: @escaping () -> T, 220 | animated: Bool? = nil) { 221 | let dialog = EmptyView() 222 | .presentAsNavigatorDialog(dialogContent: view) 223 | .ignoresSafeArea() 224 | .background(Color.black.opacity(0.0)) 225 | let hostingController = UIHostingController(rootView: dialog) 226 | hostingController.view.backgroundColor = .clear 227 | hostingController.modalPresentationStyle = .overCurrentContext 228 | guard let topViewController = UIApplication.shared.topViewController() else { return } 229 | topViewController.present(hostingController, animated: false) 230 | } 231 | 232 | /// Present an action sheet above the current view. 233 | /// 234 | /// - Parameters: 235 | /// - actionSheet: A closure that returns the action sheet content. 236 | /// - animated: Optional. Whether to animate the presentation. Default is false. 237 | public func presentActionSheet(actionSheet: @escaping () -> ActionSheet, 238 | animated: Bool? = nil) { 239 | let actionSheet = EmptyView() 240 | .presentAsNavigatorActionSheet(actionSheetContent: actionSheet) 241 | .ignoresSafeArea() 242 | .background(Color.clear) 243 | let hostingController = UIHostingController(rootView: actionSheet) 244 | hostingController.view.backgroundColor = .clear 245 | hostingController.view.isOpaque = false 246 | hostingController.modalPresentationStyle = .overCurrentContext 247 | guard let topViewController = UIApplication.shared.topViewController() 248 | else { 249 | return 250 | } 251 | topViewController.present(hostingController, animated: false) 252 | } 253 | 254 | /// Pop to a specific view type in the navigation stack. 255 | /// 256 | /// - Parameters: 257 | /// - typeOfView: The type of view to pop to. 258 | /// - animated: Optional. Whether to animate the transition. Default is true. 259 | /// - inPosition: Optional. The position type to search for the view. Default is .last. 260 | public func popToView(_ typeOfView: T.Type, 261 | animated: Bool? = nil, 262 | inPosition: PopPositionType? = .last) { 263 | let nav = NavigationManager.getCurrentNavigationController() 264 | 265 | switch inPosition { 266 | case .last: 267 | if let vc = nav?.viewControllers.last(where: { $0 is UIHostingController }) { 268 | nav?.popToViewController(vc, animated: animated ?? true) 269 | } 270 | case .first: 271 | if let vc = nav?.viewControllers.first(where: { $0 is UIHostingController }) { 272 | nav?.popToViewController(vc, animated: animated ?? true) 273 | } 274 | default: 275 | break 276 | } 277 | } 278 | 279 | /// Get the current root view of the app. 280 | /// 281 | /// - Returns: The root view as a `RootApp` if found, otherwise nil. 282 | public func getCurrentView() -> RootApp? { 283 | let nav = NavigationManager.getCurrentNavigationController() 284 | if let viewController = nav?.viewControllers.last, 285 | let hostingController = viewController as? UIHostingController> { 286 | return hostingController.rootView 287 | } 288 | return nil 289 | } 290 | 291 | } 292 | 293 | /// Utility functions for finding and retrieving the current navigation controller. 294 | extension NavigationManager { 295 | /// Recursively searches for a navigation controller in the view controller hierarchy. 296 | /// 297 | /// - Parameter viewController: The view controller to start the search from. 298 | /// - Returns: The found `UINavigationController` or `nil` if not found. 299 | private static func findNavigationController(viewController: UIViewController?) -> UINavigationController? { 300 | guard let viewController = viewController else { 301 | return nil 302 | } 303 | 304 | if let navigationController = viewController as? UINavigationController { 305 | return navigationController 306 | } 307 | 308 | for childViewController in viewController.children { 309 | return findNavigationController(viewController: childViewController) 310 | } 311 | 312 | return nil 313 | } 314 | 315 | /// Retrieves the current navigation controller for the app. 316 | /// 317 | /// - Returns: The current `UINavigationController` or `nil` if not found. 318 | private static func getCurrentNavigationController() -> UINavigationController? { 319 | let nav = findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController) 320 | return nav 321 | } 322 | 323 | } 324 | 325 | --------------------------------------------------------------------------------