├── .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 |
--------------------------------------------------------------------------------