Open source software isn't great because it's free. It's great because it's made with love and enthusiasm.
frzi / swiftui-router Goto Github PK
View Code? Open in Web Editor NEWPath-based routing in SwiftUI
License: MIT License
Path-based routing in SwiftUI
License: MIT License
Open source software isn't great because it's free. It's great because it's made with love and enthusiasm.
My requirement is:
Switch to another tab then push to a certain view under navigation view then finally present a modal?
How can I config routing for this case?
Thanks
Have SwifUIRouter in packages. Error during build on CI:
xcodebuild[70128:54646963] [MT] IDEFileReferenceDebug: [Load] <IDESwiftPackageCore.IDESwiftPackageSpecialFolderFileReference, 0x6000007fbb00: name:SwiftUIRouter.docc path:group:SwiftUIRouter.docc> Failed to load container at path: /Users/gitlab-runner/Library/Developer/Xcode/DerivedData/iosApp-asfxiubzkohtrkgwfpdkjwugwcjj/SourcePackages/checkouts/SwiftUIRouter/Sources/SwiftUIRouter.docc, Error: Error Domain=com.apple.dt.IDEContainerErrorDomain Code=6 "Cannot open "SwiftUIRouter.docc" as a "Swift Package Folder" because it is already open as a "Folder"." UserInfo={NSLocalizedDescription=Cannot open "SwiftUIRouter.docc" as a "Swift Package Folder" because it is already open as a "Folder".}
Have spent a lot of time to fix it. Possibly this is Xcode bug.
kean/Nuke#609 (comment)
Guys who faced with similar issue just removed .docc folder from project while it is not fixed by Apple. Can we do the same for SwiftUIRouter? have no ideas at all how to fix it
A much needed package, thank you for bringing this to the community!
Sadly, I ran into an issue on my first run testing things out. It's probably not the most common series of events but here's my setup:
Router {
SwitchRoutes {
Route("start") {
StartView()
}
Route {
NavLink(to: "start") {
Text("Go to start")
}
}
}
}
When app opens, it goes to the catch-all route (as expected).
Pressing NavLink takes me to StartView (as expected).
StartView uses Navigator EO and a button with one line: navigate(to: "overview")
. Tapping that button opens the catch-all route (as expected).
When tapping NavLink again I expected it to go to the StartView again. Sometimes it navigates to a blank page and sometimes nothing occurs at all.
Maybe this is the outside the intended functionality, if so I wonder if the documentation could better reflect that.
EDIT:
Initially the only result on Step 4 was navigating to a blank screen, so I thought this might be due to how I'm handling my onAppear/onDisappear in StartView. But now it's just staying out and not navigating at all.
I even tried a slightly different example...
Router {
SwitchRoutes {
Route("start") {
StartView()
}
Route("overview") {
NavLink(to: "info") {
Text("Go to info")
}
}
Route {
NavLink(to: "start") {
Text("Go to start")
}
}
}
}
Now it gets to the catch all from "overview", but again nothing happens when tapping NavLink to go to "start".
How to switch to a different tab view.
From Route, i can get expected tab via parameters.tab
, however, how can i hand over this value to @State $selection
?
@State private var selection = 0
Route(path: "tabView/:tab") { info in
self.myTabView(activeTab: info.parameters.tab ?? "")
}
TabView(selection: $selection) {
...
}
Thanks for building this prototype! I learned quite a bit digging through it.
I've been trying to figure out a solution for routing in a fairly large app. I was hoping this type of approach might work, but after digging in it looks to be too limited.
Am I correct in my assumption that I could implement multiple Routers in a project (for each tab, for instance) but that the max amount of Routes per base view would be 10? So if I had a view that had 11 potential destination views, this would not work?
Hoping I'm missing some obvious workaround. Appreciate any of your input. Thanks again!
I'm not really sure if it is something in the lib or I just messed things.
When I try something like:
ForEach(pipes, id: \.id) { pipe in
Link(to: "/pipe/:" + pipe.id) {
PipeCell(pipe: pipe).frame(
width: pipeCellSize.width,
height: pipeCellSize.height,
alignment: .center
)
}
}
I get this:
Link
seems to display this weird white rectangle behind the view. I'm not sure how I should use it.
Currently, I'm doing this:
enum AppRoutes {
static let splash = "/"
static let login = "/login"
static let feed = "/feed"
}
SwitchRoutes {
Route(AppRoutes.splash) {
SplashScreenView()
}
Route(AppRoutes.feed) {
FeedView()
}
...
}
Is there any way to make this properly type safe, or at least safer? It seems rather fragile to rely on strings in this manner. Ideally, I wouldn't want to define my own "route enum" and have navigator.navigate
accept only that type, etc.
What's the proper way to set the navigation path on app launch, e.g. based on whether the user is logged in to some API?
How can I Create one more navigation stack above tabView?
So I need to present a few different flows under each of tab. But I can't fine approach to do that. I had a look in examples and there is no scenario I need.
I'll explain using one of examples:
From one of the tabs I what to navigate further:
How can I push View above tabView, not inside?
Please help)
We are using this library since starting the development of our app. It grew quite big and we start having smaller annoying issues that would not exist if it was for the standard NavigationLink.
I had to think about how a path-based routing approach could work with the standard NavigationLink in SwiftUI and there are a lot of things to consider, of course.
Now, before I go ahead and try to open a PR that allows using NavigationLink (optionally, or even make it generic and allow custom implementations of navigators?), I wanted to ask whether someone has experience with that.
Especially @frzi . Is there a reason why you opted for a custom implementation other than convenience? Did you try and see some problems or do you know what I should focus on when trying?
I expect back stack to work same as in system navigation: view states from the back stack should be saved.
It includes saving @State
and @StateObject
annotated objects, but I can think of workaround how I can store them manually.
The main think is scroll view position - in SwiftUI it's impossible to scroll to contentOffset
, if we're using basic ScrollView
, so I can't even store scroll position and restore it when view appears, unless I write my own scroll view.
Here's a basic example to reproduce:
@EnvironmentObject private var navigator: Navigator
var body: some View {
SwitchRoutes {
Route("/settings") {
Button {
navigator.goBack()
} label: {
Text("back")
}
}
Route {
Text(navigator.path)
Button {
navigator.navigate("/settings")
} label: {
Text("next")
}
ScrollView {
ForEach(0..<100, id: \.self) { i in
Text("\(i)")
}
}
}
}
}
A possible workaround would be having all the screens from the back stack in the view tree - e.g. in ZStack
, one on top of an other one. But this for sure would break onAppear
/onDisappear
, as well as transitions.
Hey! Great work on this library. Really hope it takes off more because it really is a great solution for routing in SwiftUI.
A minor bug I noticed is that while wildcards do not rely on trailing slashes, optional parameters do. So for example, I had
Route("/chats/*")
which would render regardless of whether I was going to /chats
or /chats/:chatID
. However, if I redefine this as /chats/:chatID?
, then navigating to /chats
produces an empty screen. Only if I navigate to /chats/
will the SwiftUI component appear.
It's a minor regex fix I imagine, but nevertheless thought I'd raise it. I can help open a fix if you don't have the time :)
Regex error is irgnored at and it can lead to long debug sessions.
In our case we had a kebab-case parameter. This parameter will be used as named capture group in regex and will throw an error NSRegularExpression Error: Error Domain=NSCocoaErrorDomain Code=2048 "The value โ^(/route-to/(?<parameter-one>[^/?]+)/(?<parameter-two>[^/?]+))$โ is invalid." UserInfo={NSInvalidValue=^(/route-to/(?<parameter-one>[^/?]+)/(?<parameter-two>[^/?]+))$}
.
Maybe it would be good to know in the README in how to use parameters.
route is ignored
/route-to/:parameter-one/:parameter-two
route is registered or throw an error / or logging information.
/route-to/:parameter-one/:parameter-two
Hi, first of all, congrats about this library, it is very useful.
I wonder whether there is a way to animate switch route changes like the old-styled UIKit NavigationView actually does. Maybe there is a easy way to do it, but I didn't find it.
Thanks in advance
I have tried without success in creating a full screen navigation that don't get clipped, is this even possible?
We still need the inner views to handle safe area insets for laying out its components, but having a slide transition between 2 views without ignoring safe area on the parent ends up with views appearing / disappearing on screen (landscape).
The only real solution I got is to read the safe area outside of the router and then pass it down as environment, however, this is not ideal because we need to rebuild all views to use the custom values + padding instead of the proper way to leverage safe area.
Hello, first of all, I do appreciate your effort. Thank you for building such an useful library. I am trying to develop a watch companion app using the swiftui-router and I am facing this issue of navigating to a black or blank?! screen. Perhaps, I may have not used the router properly...I still do need help with the correct implementation of the library.
here are sample codes similar to for what I have done so far:
import SwiftUI
import SwiftUIRouter
struct RouteURL {
static let authentication = "/auth"
static let standby = "/auth/standby"
static let activity = "/auth/activity"
static let complete = "/auth/complete"
}
@main
struct WatchDemo_Watch_AppApp: App {
var body: some Scene {
WindowGroup {
Router {
RootView()
}
}
}
}
import SwiftUI
import SwiftUIRouter
struct RootView: View {
@EnvironmentObject var navigator: Navigator
var body: some View {
VStack {
Route(RouteURL.authentication) {
AuthenticationView()
.onAppear {
print("authentication Appear")
}
}
}
.onAppear {
navigator.navigate(RouteURL.authentication)
}
}
}
import SwiftUI
import SwiftUIRouter
enum LoginState: Equatable {
case attemptToLogin
case login
case logout
}
struct AuthenticationView: View {
@State private var loginState: LoginState = .attemptToLogin
@EnvironmentObject var navigator: Navigator
var body: some View {
VStack {
switch loginState {
case .attemptToLogin:
Text("attempt to login")
ProgressView()
case .login:
Route {
ActivityRouteView()
}
case .logout:
Text("logout")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.loginState = .logout
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.loginState = .login
}
}
}
}
#Preview {
Router {
AuthenticationView()
}
}
import SwiftUI
import SwiftUIRouter
struct ActivityRouteView: View {
@EnvironmentObject var navigator: Navigator
var body: some View {
VStack {
Route(RouteURL.standby, content: standby)
Route(RouteURL.activity, content: activity)
Route(RouteURL.complete, content: complete)
}
.onAppear {
DispatchQueue.main.async {
navigator.navigate(RouteURL.standby)
print("navigate to standby")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
navigator.navigate(RouteURL.activity)
print("navigate to activity")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
navigator.navigate(RouteURL.complete)
print("navigate to complete")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
navigator.navigate("\\", replace: true)
print("to root")
}
}
}
var standby: some View {
Color.blue
}
var activity: some View {
Color.red
}
var complete: some View {
Color.yellow
}
}
#Preview {
Router {
ActivityRouteView()
}
}
At first, I thought it was a nothing but a preview bug, however, in a real project with a similar router hierarchy, same issue occurs.
I was expecting to see the color change as below when changing route from authentication to activity and standby views...perhaps it may be a bug, or I might have not used the router correctly.
Please help!!
Hello, I am new to SwiftUI, I have a page A with a Button, when I press the Button, I want navigate to PageB, I don't know how to write the Route, can you help me, thanks.
Hello, thank you for the library. But because there is no example, I don't know how to use it. Can you provide an example? thanks
As seen, I successfully run with it on my actual device, but failed in preview.
Am I made something wrong?
MacOS BigSur 11.6 (20G165)
XCode Version 13.0 (13A233)
Swift 5.5
Just simple code
import SwiftUI
import SwiftUIRouter
struct Simple: View {
var body: some View {
NavLink(to: "/welcome") {
Text("Enter")
}
}
}
struct Simple_Previews: PreviewProvider {
static var previews: some View {
Simple()
}
}
No ObservableObject of type Navigator found. A View.environmentObject(_:) for Navigator may be missing as an ancestor of this view.
----------------------------------------
MissingEnvironmentObjectError: Preview is missing environment object "Navigator"
[Sample] crashed due to missing environment of type: Navigator. To resolve this add `.environmentObject(Navigator(...))` to the appropriate preview.
Process: [Sample][65512]
Date/Time: 2021-09-29 01:02:26 +0000
Log File: <none>
Application Specific Information:
[
dyld4 config: DYLD_ROOT_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot DYLD_LIBRARY_PATH=/Users/apple/Library/Developer/Xcode/DerivedData/[Sample]-dcfjwaohgmozctgpvtbixewazoqo/Build/Intermediates.noindex/Previews/[Sample]/Products/Debug-iphonesimulator DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot//System/Library/PrivateFrameworks/PreviewsInjection.framework/PreviewsInjection DYLD_FRAMEWORK_PATH=/Users/apple/Library/Developer/Xcode/DerivedData/[Sample]-dcfjwaohgmozctgpvtbixewazoqo/Build/Intermediates.noindex/Previews/[Sample]/Products/Debug-iphonesimulator
SwiftUI/EnvironmentObject.swift:70: Fatal error: No ObservableObject of type Navigator found. A View.environmentObject(_:) for Navigator may be missing as an ancestor of this view.
CoreSimulator 776.3 - Device: iPhone 12 Pro (48F2C8C7-D179-42EE-8709-82EB7E969381) - Runtime: iOS 15.0 (19A339) - DeviceType: iPhone 12 Pro
]
==================================
| MessageSendFailure: Message send failure for send previewInstances message to agent
|
| ==================================
|
| | RemoteHumanReadableError: Cannot send message on <UVAgentPreviewServiceServerConnection: 0x600001ee54a0>: connection has invalidated
| |
| | com.apple.dt.UITestingAgent (-1):
Sometimes there are blank pages when going two levels deep. When going back to the previous screen it too is blank. Not always reproducible.
Obviously, without Animations, SwiftUIRouter does not provide native-like back swipes in the navigation hierarchy. Do you know any good way to replicate those edge pan swipes to feel native?
In iOS, swipe is a really important feature and ignoring it is not really an option. That being said using path-based routing offers great benefits over the traditional routing model in SwiftUI. Combining both would be ideal.
Currently, I am using a somewhat hacky solution which is really not nice at all compared with the native feel of back swipes:
struct DefaultBackSwipeGesture: ViewModifier {
@EnvironmentObject private var navigator: Navigator
@GestureState private var dragOffset = CGSize.zero
func body(content: Content) -> some View {
content.gesture(DragGesture().updating($dragOffset, body: { (value, state, transaction) in
if(value.startLocation.x < 20 && value.translation.width > 100) {
navigator.goBack(total: 1)
}
}))
}
}
extension View {
func defaultBackSwipeGesture() -> some View {
return modifier(DefaultBackSwipeGesture())
}
}
as well as the animation ViewModifier
as in your example in the docs.
This leads to a fake swipe where the animation transition to the last View happens once you have done a swipe far enough starting from the left edge.
This is obviously not a real swipe, which means you can't stop at some point or swipe (drag) back and forth a little bit without releasing your finger. It just starts the animation at some points and that's it.
If you don't look closely that's enough. But it's certainly not the best ever user experience. Is there a way to combine the real NavigationView
back swipes with this library at all? I imagine replicating it from scratch is too much work for too little outcome. Can we somehow reuse/abuse NavigationView
exactly for that?
Imagine the following navigation pattern:
/screen1/:id1
/screen1/:id1/:childId1
/screen1/:id1/:childId1/subchildId1
...
And I want all these paths to be routed to a single screen (it's like going down a filesystem).
I've tried doing
screen1/* - this catches all these navigations but doesn't give me parameters
screen1/*/:id - doesn't work
:id - doesn't work for nested navigations either
Hence I'm taking a temporary solution of forking the library and making RouteInformation.matchedPath public to analyze it in code (which is not the best way to solve this, of course).
What do you think would be the proper way to support this (fairly common I guess) scenario?
Thanks!
Since routes know exactly what the user is doing at any given time, it would be pretty awesome to have some automatic integration with NSUserActivity to provide apps using SwiftUIRouter with some low effort integration with Siri and such on platforms that support NSUserActivity
Please provide information or example how to switch between different flows in case when one of the flow contains TabView.
So I have registration flow and main flow with tab bar (I use original swiftUI TabView with TabContents)
And I can't find approach how I can navigate to tabView after registration flow is completed. I get empty TabView. I suppose that's because tabView is a set of routes(views inside) and if I use tabView as root view it's okey. But if I use some empty root view as router that navigates to registration or tabView flow it can't navigate correctly to tabView
Would be nice for beginners (like me) how to install this swift package
I followed the doc Animating Routes and tried to replace the transition with .opacity
, but it doesn't work. When using .move(edge: .bottom).combined(with: .opacity)
, only the move
transition is executed.
Hi and thanks for a very nice library!
I am wondering if it would be possible to navigate between two
and three
without one
being rerendered?
The way we use this is that the view model of one
handles data fetching and is then passed to two
(and three
) via .environemntObject()
. But when navigating between those one
is recreated on all navigation steps as well.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Router(initialPath: "/one/two/three") {
Route(path: "/one/*") {
Text("One")
Route(path: "/one/two/*") {
Text("Two")
Route(path: "/one/two/three") {
Text("Three")
}
}
}
}
}
}
}
Is there a way to keep some paths loaded in memory, i.e. not recreate the views when the path changes?
I was implementing a tab bar layout like in the RandomUsers example, but with this solution the pages get recreated every time the user switches tab, which can make the process of switching tab a bit laggy.
Is there a way to keep those pages in memory to provide a smooth tab bar experience?
Note: this would also allow keeping states, like scrolling positions. It just feels more natural.
The code below resolves the swift 10 direct ancestor limitation. If you think it is a good option you could introduce it in the example.
import SwiftUI
import SwiftUIRouter
struct RouterModel: Identifiable {
var id = UUID()
var path: String
var view: AnyView
}
struct Routes: View {
let routes:[RouterModel] = [
RouterModel(path: "/page1", view: AnyView(Page1View())),
RouterModel(path: "/page2", view: AnyView(Page2View())),
RouterModel(path: "/page3", view: AnyView(Page3View())),
];
var body: some View {
Router {
VStack {
ContentView(routes: self.routes)
}
}
}
}
private func ContentView(routes: [RouterModel]) -> some View {
Switch {
ForEach(routes) { route in
Route(path: route.path, exact: true) { _ in
AnyView(route.view)
}
}
// Main view
Route {
MainPageView()
}
}
}
Love the library! Im struggling to get my view to update when the parameter changes
Route(path: "/page/:id", exact: true) { route in
return PageView(id: Int(route.parameters.id!)!)
}
first time i route to "/page/5" i can get the id and perform my api calls, however if then open "/page/6" the view isn't refreshed and i still am still looking at "/page/5". How did you resolve this issue?
struct PageView: View {
@State var id: Int = 0
var body: some View {
VStack{
Text("\(id)")
}
}
Hi,
We are using SwiftUIRouter in our application but some usecases came up where we programmatically route using
navigator.navigate
and would like to have control over the resulting NavigationAction
s direction
since we use that to animate the route transition and sometimes we want to go 'deeper' but route-wise the code will classify it as 'sideways'.
An example would be navigating from /auth/login
to /auth/reset/mail
to reset/auth/reset/confirm
where we would like to go .deeper
in that order and .higher
in reverse.
Maybe navigator.navigate
could accept an optional direction parameter to override the default behaviour. @frzi let me know what you think. In case this is a good idea I'd be happy to open a PR if it helps.
Very randomly, without any consistency, I see that Navigate tries to navigate to a route, then chooses to make a wrong turn to the default route in the last step (inside a SwitchRoutes) and then repeats itself a few times until SwiftUI decides to stop it and renders an empty screen.
I had this issue multiple times and most of the time it was an endless loop caused by my code. This time, however, I even checked the info parameter and can see that it took a wrong turn, even though the route is set correctly.
I can't really provide a sample project as it is anyways very random and happens just sometimes. I was just hoping that someone experienced this as well and knows where I can start debugging.
Just as a presentation of what I mean, see the below:
struct MainRouter: RouterView {
var content: some View {
Router(initialPath: "/sign-in") {
SwitchRoutes {
Route("/sign-up/*") {
SignUpRouter()
}
Route("/sign-in/*") {
SignInRouter()
}
Route("/home/*") {
HomeRouter()
}
Route {
Navigate(to: "/sign-in")
}
}
}
}
}
// --- Removing all the other Routers for brevity. This only happens on HomeRouter for some reason.
struct HomeRouter: RouterView {
var content: some View {
SwitchRoutes {
Route("root") {
SomeHomeView()
}
Route("profile") {
SomeProfileView()
}
Route { info in
// The case is described below. info tells me that the route is /home/profile but for some reason it lands here in the fallback.
Navigate(to: "/home/root")
}
}
}
}
So the issue arises if I navigate to "/home/profile" (or any other subview of home). What happens instead (again, just sometimes), is that the Navigator jumps to the fallback Route {}. The weird thing is that the info variable has the right path saved. For some reason, it just doesn't match the right one when selecting the path to navigate to.
I know that this can be caused by literally everything and probably depends on my app state. Still good to have this open and see if anyone else is experiencing this.
In general, I would like to understand how this situation CAN happen with SwiftUIRouter. Once I do, it will be much easier to debug and solve.
Hi, I am using this library.
I cannot find out how to use nested route, so is it able to use nested route?
If available, could you give me some examples?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.