β¨ LinkNavigator is a library that helps you easily navigate between pages in SwiftUI.
- LinkNavigator provides an intuitive syntax for navigating pages via URL path-like expressions.
- You can easily go to any page with the deep-link processing style.
- You can inject parameters with page transition.
- LinkNavigator is designed for use in Uni-directional Architecture such as MVI design pattern or The Composable Architecture from pointfreeco, but it can be used in other architectures as well.
The following translations of this README have been contributed by members of the community:
If you'd like to contribute a translation, please open a PR with a link to a Gist!
-
push one or many pages.
navigator.next(paths: ["page1", "page2"], items: [:], isAnimated: true)
-
pop one or many pages.
navigator.remove(paths: ["pageToRemove"])
-
back to the prior page or dismiss modal simply.
navigator.back(isAnimated: true)
-
go to the page you want. If that page is already within navigation stack, go back to that page. Else if that page is not within stack, push new one.
navigator.backOrNext(path: "targetPage", items: [:], isAnimated: true)
-
replace current navigation stack with new one.
navigator.replace(paths: ["main", "depth1", "depth2"], items: [:], isAnimated: true)
-
open page as sheet or full screen cover.
navigator.sheet(paths: ["sheetPage"], items: [:], isAnimated: true) navigator.fullSheet(paths: ["page1", "page2"], items: [:], isAnimated: true, prefersLargeTitles: false)
-
close a modal and call completion closure.
navigator.close(isAnimated: true) { print("modal dismissed!") }
-
show a system alert.
let alertModel = Alert( title: "Title", message: "message", buttons: [.init(title: "OK", style: .default, action: { print("OK tapped") })], flagType: .default) navigator.alert(target: .default, model: alertModel)
-
edit complicated paths and use it.
// current navigation stack == ["home", "depth1", "depth2", "depth3"] // target stack == ["home", "depth1", "newDepth"] var new = navigator.range(path: "depth1") + ["newDepth"] navigator.replace(paths: new, items: [:], isAnimated: true)
-
control pages behind modal.
navigator.rootNext(paths: ["targetPage"], items: [:], isAnimated: true) navigator.rootBackOrNext(path: "targetPage", items: [:], isAnimated: true)
-
you can choose modal presentation styles for iPhone and iPad respectively.
navigator.customSheet( paths: ["sheetPage"], items: [:], isAnimated: true, iPhonePresentationStyle: .fullScreen, iPadPresentationStyle: .pageSheet, prefersLargeTitles: .none)
-
forcely reload the last page behind the modal. This is useful when you need to call the onAppear(perform:) again.
navigator.rootReloadLast(items: [:], isAnimated: false)
LinkNavigator provides 2 Example Apps.
-
To install LinkNavigator in your SwiftUI project, you need to implement 4 files.
-
You can freely edit the type names. In the following examples, simple names are used for clarity.
-
Describe in order: AppDependency -> AppRouterGroup -> AppDelegate -> AppMain
// AppDependency.swift // A type that manages external dependencies. import LinkNavigator struct AppDependency: DependencyType { } // you need to adopt DependencyType protocol here.
// AppRouterGroup.swift // A type that manages the pages you want to go with LinkNavigator. import LinkNavigator struct AppRouterGroup { var routers: [RouteBuilder] { [ HomeRouteBuilder(), // to be implemented in Step 3 Page1RouteBuilder(), Page2RouteBuilder(), Page3RouteBuilder(), Page4RouteBuilder(), ] } }
// AppDelegate.swift // A type that manages the navigator injected with external dependencies and pages. import SwiftUI import LinkNavigator final class AppDelegate: NSObject { var navigator: LinkNavigator { LinkNavigator(dependency: AppDependency(), builders: AppRouterGroup().routers) } } extension AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { true } }
// AppMain.swift // A type that sets the starting page of the Application. import SwiftUI import LinkNavigator @main struct AppMain: App { @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate var navigator: LinkNavigator { appDelegate.navigator } var body: some Scene { WindowGroup { navigator .launch(paths: ["home"], items: [:]) // the argument of 'paths' becomes starting pages. .onOpenURL { url in // in case you need deep link navigation, // deep links should be processed here. } } } }
-
Add a
navigator
property inside the page struct type, so that it is injected when initialized. -
Depending on the characteristics of the architecture, freely change the position of the navigator property and use it. For example, you can put it in
ViewModel
orEnvironment
.struct HomePage: View { let navigator: LinkNavigatorType var body: some View { ... } }
-
Create a struct type adopting the
RouteBuilder
protocol for every page. -
RouteBuilder structs created in this way are collected and managed in the AppRouterGroup type.
import LinkNavigator import SwiftUI struct HomeRouteBuilder: RouteBuilder { var matchPath: String { "home" } var build: (LinkNavigatorType, [String: String], DependencyType) -> MatchingViewController? { { navigator, items, dependency in return WrappingController(matchPath: matchPath) { HomePage(navigator: navigator) } } } }
LinkNavigator supports Swift Package Manager.
File
menu at the top of Xcode -> SelectAdd Packages...
.- Enter "https://github.com/interactord/LinkNavigator.git" in the Package URL field to install it.
- or, add the following in the
Package.swift
.
let package = Package(
name: "MyPackage",
products: [
.library(
name: "MyPackage",
targets: ["MyPackage"]),
],
dependencies: [
.package(url: "https://github.com/interactord/LinkNavigator.git", .upToNextMajor(from: "0.6.1"))
],
targets: [
.target(
name: "MyPackage",
dependencies: ["LinkNavigator"])
]
)
- Q: How can I use large titles in SwiftUI?
/// in AppMain.swift (MVI)
/// To use for route navigation, set the prefersLargeTitles parameter to true in the launch method.
navigator
.launch(paths: ["home"], items: [:], prefersLargeTitles: true)
/// in HomeView.swift (MVI)
/// To specify the display mode of the navigation bar title, use the navigationBarTitleDisplayMode (.line, .large, .automatic) in the SwiftUI screen of each screen.
ScrollView {
....
}
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Home")
/// If you want to use it in fullSheet or customSheet,
/// Home.intent (MVI)
/// To enable large titles, set the prefersLargeTitles variable to true. To maintain the current settings, use .none.
navigator.fullSheet(paths: ["page1", "page2"], items: [:], isAnimated: true, prefersLargeTitles: true)
-
Q: I'm wondering how to apply IgnoringSafeArea to a specific part or the entire screen if I want to?
- Add the following code to the screen where LinkNavigator is first started (example: AppMain.swift).
- Then, add the following example code. (Refer to the AppMain.swift example.)
navigator
.launch(paths: ["home"], items: [:], prefersLargeTitles: true)
/// - Note:
/// If you are using the ignoresSafeArea property to ignore the safe area on an internal screen,
/// please add the corresponding code to the part where you first execute the LinkNavigator.
.ignoresSafeArea()
-
Q: In the view controller, I need to handle various tasks such as navigation or calling Firebase events when calling the screen. How should I handle it?
- You can customize the WrappingController. I will provide an example code for customization.
import SwiftUI
public final class DebugWrappingViewController<Content: View>: UIHostingController<Content>, MatchPathUsable {
// MARK: Lifecycle
public init(
matchPath: String,
trackEventUseCase: TrackEventUseCase,
@ViewBuilder content: () -> Content)
{
self.matchPath = matchPath
self.eventSubscriber = eventSubscriber
self.trackEventUseCase = trackEventUseCase
super.init(rootView: content())
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("βοΈ \(matchPath) deinit...")
}
// MARK: Public
public let matchPath: String
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = SystemColor.Background.Default.Base.getColor()
print("π \(matchPath)")
trackEventUseCase.sendEvent(.screen(matchPath))
}
// MARK: Private
private let trackEventUseCase: TrackEventUseCase
}
This library is released under the MIT license. See LICENSE for details.