Skip to content

Commit 5e69c68

Browse files
authored
Merge pull request #38 from frzi/dev/docs
Dev/docs
2 parents 42ef6d3 + df6182b commit 5e69c68

10 files changed

+122
-37
lines changed

Docs/AnimatingRoutes.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Animating routes
22

3+
Simulating screen transitions à la iOS.
4+
5+
## Introduction
6+
37
On a platform like iOS, users may expect animated screen transitions when navigating through the app. (Less so the case with macOS) Apps get these transitions for free with `NavigationView`. But with SwiftUI Router, however, this is not the case. Ideally, you want a transition that differs as the user goes forward (deeper) in the app and when they go back (higher).
48

59
SwiftUI Router exposes the `Navigator` environment object. An object that allows for navigation done programmatically. It also contains the property `.lastAction`, which is of type `NavigationAction?`. This object contains read-only information about the last navigation that occurred. Information like the previous path, the current path, whether the app navigated forward or back. But also the *direction* of the navigation, which is what we're interested in right now.
@@ -16,7 +20,7 @@ struct NavigationTransition: ViewModifier {
1620

1721
func body(content: Content) -> some View {
1822
content
19-
.animation(.easeInOut)
23+
.animation(.easeInOut, value: navigator.path)
2024
.transition(
2125
navigator.lastAction?.direction == .deeper || navigator.lastAction?.direction == .sideways
2226
? AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
@@ -39,19 +43,19 @@ extension View {
3943

4044
The modifier can be applied to `Route` views:
4145
```swift
42-
Route(path: "news") {
46+
Route("news") {
4347
NewsScreen()
4448
}
4549
.navigationTransition()
4650
```
4751

48-
The modifier can also be applied to a `SwitchRoutes`. This will apply the animated transition to all `Route` views inside the `SwitchRoutes`.
52+
The modifier can also be applied to a ``SwitchRoutes``. This will apply the animated transition to all `Route` views inside the ``SwitchRoutes``.
4953
```swift
5054
SwitchRoutes {
51-
Route(path: "news/:id", validator: newsIdValidator) { uuid in
55+
Route("news/:id", validator: newsIdValidator) { uuid in
5256
NewsItemScreen(uuid: uuid)
5357
}
54-
Route(path: "news") {
58+
Route("news") {
5559
NewsScreen()
5660
}
5761
Route {

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.5
22

33
import PackageDescription
44

README.md

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
<img src="Docs/Images/logo.svg" alt="SwiftUI Router" width="600">
22

3-
> Easy and maintainable app navigation with path based routing for SwiftUI.
3+
> Easy and maintainable app navigation with path-based routing for SwiftUI.
44
55
![SwiftUI](https://img.shields.io/github/v/release/frzi/SwiftUIRouter?style=for-the-badge)
66
[![SwiftUI](https://img.shields.io/badge/SwiftUI-blue.svg?style=for-the-badge&logo=swift&logoColor=black)](https://developer.apple.com/xcode/swiftui)
77
[![Swift](https://img.shields.io/badge/Swift-5.3-orange.svg?style=for-the-badge&logo=swift)](https://swift.org)
88
[![Xcode](https://img.shields.io/badge/Xcode-13-blue.svg?style=for-the-badge&logo=Xcode&logoColor=white)](https://developer.apple.com/xcode)
99
[![MIT](https://img.shields.io/badge/license-MIT-black.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
1010

11-
With **SwiftUI Router** you can power your SwiftUI app with path based routing. By utilizing a path based system, navigation in your app becomes more flexible and easier to maintain.
11+
With **SwiftUI Router** you can power your SwiftUI app with path-based routing. By utilizing a path-based system, navigation in your app becomes more flexible and easier to maintain.
1212

1313
## Index
1414
* [Installation](#installation-)
1515
* [Documentation](#documentation-)
16+
* [Examples](#examples-)
1617
* [Usage](#usage-)
1718
* [License](#license-)
1819

@@ -31,7 +32,15 @@ import SwiftUIRouter
3132
<br>
3233

3334
## Documentation 📚
34-
- [Animating routes](/Docs/AnimatingRoutes.md)
35+
- [Animating routes](/Sources/SwiftUIRouter/AnimatingRoutes.md)
36+
37+
<br>
38+
39+
## Examples 👀
40+
- [SwiftUI Router Examples](https://github.com/frzi/SwiftUIRouter-Examples) contains:
41+
[RandomUsers](https://github.com/frzi/SwiftUIRouter-Examples/tree/main/RandomUsers)
42+
[Swiping](https://github.com/frzi/SwiftUIRouter-Examples/tree/main/Swiping)
43+
[TabViews](https://github.com/frzi/SwiftUIRouter-Examples/tree/main/TabViewRouting)
3544

3645
<br>
3746

@@ -50,13 +59,13 @@ The entry of a routing environment. Wrap your entire app (or just the part that
5059

5160
### `Route`
5261
```swift
53-
Route(path: "news/*") {
62+
Route("news/*") {
5463
NewsScreen()
5564
}
56-
Route(path: "settings") {
65+
Route("settings") {
5766
SettingsScreen()
5867
}
59-
Route(path: "user/:id?") { info in
68+
Route("user/:id?") { info in
6069
UserScreen(id: info.parameters["id"])
6170
}
6271
```
@@ -72,7 +81,7 @@ func validateUserID(routeInfo: RouteInformation) -> UUID? {
7281
UUID(routeInfo.parameters["id"] ?? "")
7382
}
7483

75-
Route(path: "user/:id", validator: validateUserID) { userID in
84+
Route("user/:id", validator: validateUserID) { userID in
7685
UserScreen(userID: userID)
7786
}
7887
```
@@ -97,13 +106,13 @@ A wrapper around a `Button` that will navigate to the given path if pressed.
97106
### `SwitchRoutes`
98107
```swift
99108
SwitchRoutes {
100-
Route(path: "latest") {
109+
Route("latest") {
101110
LatestNewsScreen()
102111
}
103-
Route(path: "article/:id") { info in
112+
Route("article/:id") { info in
104113
NewsArticleScreen(articleID: info.parameters["id"]!)
105114
}
106-
Route(path: ":unknown") {
115+
Route(":unknown") {
107116
ErrorScreen()
108117
}
109118
Route {

Sources/Navigate.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import SwiftUI
77

88
/// When rendered will automatically perform a navigation to the given path.
99
///
10-
/// This view allows you to pragmatically navigate to a new path in a View's body.
10+
/// This view allows you to programmatically navigate to a new path in a View's body.
1111
///
1212
/// ```swift
1313
/// SwitchRoutes {
14-
/// Route(path: "news") { NewsView() }
14+
/// Route("news", content: NewsView())
1515
/// Route {
16-
/// // If this Route gets rendered redirect
16+
/// // If this Route gets rendered it'll redirect
1717
/// // the user to a 'not found' screen.
1818
/// Navigate(to: "/not-found")
1919
/// }
@@ -31,6 +31,7 @@ public struct Navigate: View {
3131
private let replace: Bool
3232

3333
/// - Parameter path: New path to navigate to once the View is rendered.
34+
/// - Parameter replace: if `true` will replace the last path in the history stack with the new path.
3435
public init(to path: String, replace: Bool = true) {
3536
self.path = path
3637
self.replace = replace

Sources/Navigator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public final class Navigator: ObservableObject {
6868
/// will be printed to the console.
6969
///
7070
/// - Parameter path: Path of the new location to navigate to.
71-
/// - Parameter replace: if `true`, will not add the current location to the history.
71+
/// - Parameter replace: if `true` will replace the last path in the history stack with the new path.
7272
public func navigate(_ path: String, replace: Bool = false) {
7373
let path = resolvePaths(self.path, path)
7474
let previousPath = self.path

Sources/Route.swift

+53-14
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SwiftUI
1111
/// When the environment path matches a `Route`'s path, its contents will be rendered.
1212
///
1313
/// ```swift
14-
/// Route(path: "settings") {
14+
/// Route("settings") {
1515
/// SettingsView()
1616
/// }
1717
/// ```
@@ -24,7 +24,7 @@ import SwiftUI
2424
///
2525
/// **Note:** Only alphanumeric characters (A-Z, a-z, 0-9) are valid for parameters.
2626
/// ```swift
27-
/// Route(path: "/news/:id") { routeInfo in
27+
/// Route("/news/:id") { routeInfo in
2828
/// NewsItemView(id: routeInfo.parameters["id"]!)
2929
/// }
3030
/// ```
@@ -39,7 +39,7 @@ import SwiftUI
3939
/// UUID(info.parameters["uuid"]!)
4040
/// }
4141
/// // Will only render if `uuid` is a valid UUID.
42-
/// Route(path: "user/:uuid", validator: validate) { uuid in
42+
/// Route("user/:uuid", validator: validate) { uuid in
4343
/// UserScreen(userId: uuid)
4444
/// }
4545
/// ```
@@ -48,13 +48,13 @@ import SwiftUI
4848
/// Every path found in a `Route`'s hierarchy is relative to the path of said `Route`. With the exception of paths
4949
/// starting with `/`. This allows you to develop parts of your app more like separate 'sub' apps.
5050
/// ```swift
51-
/// Route(path: "/news") {
51+
/// Route("/news") {
5252
/// // Goes to `/news/latest`
5353
/// NavLink(to: "latest") { Text("Latest news") }
5454
/// // Goes to `/home`
5555
/// NavLink(to: "/home") { Text("Home") }
5656
/// // Route for `/news/unknown/*`
57-
/// Route(path: "unknown/*") {
57+
/// Route("unknown/*") {
5858
/// // Redirects to `/news/error`
5959
/// Navigate(to: "../error")
6060
/// }
@@ -79,7 +79,7 @@ public struct Route<ValidatedData, Content: View>: View {
7979
/// - Parameter validator: A function that validates and transforms the route parameters.
8080
/// - Parameter content: Views to render. The validated data is passed as an argument.
8181
public init(
82-
path: String = "*",
82+
_ path: String = "*",
8383
validator: @escaping Validator,
8484
@ViewBuilder content: @escaping (ValidatedData) -> Content
8585
) {
@@ -88,6 +88,15 @@ public struct Route<ValidatedData, Content: View>: View {
8888
self.validator = validator
8989
}
9090

91+
@available(*, deprecated, renamed: "init(_:validator:content:)")
92+
public init(
93+
path: String,
94+
validator: @escaping Validator,
95+
@ViewBuilder content: @escaping (ValidatedData) -> Content
96+
) {
97+
self.init(path, validator: validator, content: content)
98+
}
99+
91100
public var body: some View {
92101
let resolvedGlob = resolvePaths(relativePath, path)
93102

@@ -96,7 +105,10 @@ public struct Route<ValidatedData, Content: View>: View {
96105

97106
if !switchEnvironment.isActive || (switchEnvironment.isActive && !switchEnvironment.isResolved) {
98107
do {
99-
if let matchInformation = try pathMatcher.match(glob: resolvedGlob, with: navigator.path),
108+
if let matchInformation = try pathMatcher.match(
109+
glob: resolvedGlob,
110+
with: navigator.path,
111+
relative: relativePath),
100112
let validated = validator(matchInformation)
101113
{
102114
validatedData = validated
@@ -128,27 +140,44 @@ public struct Route<ValidatedData, Content: View>: View {
128140
public extension Route where ValidatedData == RouteInformation {
129141
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
130142
/// - Parameter content: Views to render. An `RouteInformation` is passed containing route parameters.
131-
init(path: String = "*", @ViewBuilder content: @escaping (RouteInformation) -> Content) {
143+
init(_ path: String = "*", @ViewBuilder content: @escaping (RouteInformation) -> Content) {
132144
self.path = path
133145
self.validator = { $0 }
134146
self.content = content
135147
}
136148

137149
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
138150
/// - Parameter content: Views to render.
139-
init(path: String = "*", @ViewBuilder content: @escaping () -> Content) {
151+
init(_ path: String = "*", @ViewBuilder content: @escaping () -> Content) {
140152
self.path = path
141153
self.validator = { $0 }
142154
self.content = { _ in content() }
143155
}
144156

145157
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
146158
/// - Parameter content: View to render (autoclosure).
147-
init(path: String = "*", content: @autoclosure @escaping () -> Content) {
159+
init(_ path: String = "*", content: @autoclosure @escaping () -> Content) {
148160
self.path = path
149161
self.validator = { $0 }
150162
self.content = { _ in content() }
151163
}
164+
165+
// MARK: - Deprecated initializers.
166+
// These will be completely removed in a future version.
167+
@available(*, deprecated, renamed: "init(_:content:)")
168+
init(path: String, @ViewBuilder content: @escaping (RouteInformation) -> Content) {
169+
self.init(path, content: content)
170+
}
171+
172+
@available(*, deprecated, renamed: "init(_:content:)")
173+
init(path: String, @ViewBuilder content: @escaping () -> Content) {
174+
self.init(path, content: content)
175+
}
176+
177+
@available(*, deprecated, renamed: "init(_:content:)")
178+
init(path: String, content: @autoclosure @escaping () -> Content) {
179+
self.init(path, content: content)
180+
}
152181
}
153182

154183

@@ -161,12 +190,19 @@ public extension Route where ValidatedData == RouteInformation {
161190
/// This object contains the resolved parameters (variables) of the `Route`'s path, as well as the relative path
162191
/// for all views inside the hierarchy.
163192
public final class RouteInformation: ObservableObject {
193+
/// The resolved path component of the parent `Route`.
194+
public let matchedPath: String
195+
196+
/// The current relative path.
164197
public let path: String
198+
199+
/// Resolved parameters of the parent `Route`s path.
165200
public let parameters: [String : String]
166201

167-
init(path: String, parameters: [String : String] = [:]) {
168-
self.path = path
202+
init(path: String, matchedPath: String, parameters: [String : String] = [:]) {
203+
self.matchedPath = matchedPath
169204
self.parameters = parameters
205+
self.path = path
170206
}
171207
}
172208

@@ -245,7 +281,7 @@ final class PathMatcher: ObservableObject {
245281
return cached!
246282
}
247283

248-
func match(glob: String, with path: String) throws -> RouteInformation? {
284+
func match(glob: String, with path: String, relative: String = "/") throws -> RouteInformation? {
249285
let compiled = try compileRegex(glob)
250286

251287
var nsrange = NSRange(path.startIndex..<path.endIndex, in: path)
@@ -278,7 +314,10 @@ final class PathMatcher: ObservableObject {
278314
}
279315

280316
let resolvedGlob = String(path[range])
317+
let matchedPath = String(path[relative.endIndex...])
281318

282-
return RouteInformation(path: resolvedGlob, parameters: parameterValues)
319+
print("resolved: \(resolvedGlob), matched: \(matchedPath), relative: \(relative)")
320+
321+
return RouteInformation(path: resolvedGlob, matchedPath: matchedPath, parameters: parameterValues)
283322
}
284323
}

Sources/Router.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import SwiftUI
1515
/// Router {
1616
/// HomeView()
1717
///
18-
/// Route(path: "/news") {
18+
/// Route("/news") {
1919
/// NewsHeaderView()
2020
/// }
2121
/// }
23 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ``SwiftUIRouter``
2+
3+
Easy and maintainable app navigation with path based routing for SwiftUI.
4+
5+
![SwiftUI Router logo](logo)
6+
7+
With **SwiftUI Router** you can power your SwiftUI app with path based routing. By utilizing a path based system, navigation in your app becomes more flexible and easier to maintain.
8+
9+
### Additional content
10+
- Examples can be found on [Github](https://github.com/frzi/SwiftUIRouter-Examples)
11+
- [Animating routes](https://github.com/frzi/SwiftUIRouter/blob/main/Docs/AnimatingRoutes.md)
12+
13+
## Topics
14+
15+
### Router
16+
17+
- ``Router``
18+
19+
### Routing
20+
21+
- ``Route``
22+
- ``SwitchRoutes``
23+
24+
### Navigating
25+
26+
- ``NavLink``
27+
- ``Navigate``
28+
29+
### Environment Objects
30+
31+
- ``Navigator``
32+
- ``RouteInformation``

Sources/SwitchRoutes.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import SwiftUI
1212
///
1313
/// ```swift
1414
/// SwitchRoutes {
15-
/// Route(path: "settings") {
15+
/// Route("settings") {
1616
/// SettingsView()
1717
/// }
18-
/// Route(path: ":id") { info in
18+
/// Route(":id") { info in
1919
/// ContentView(id: info.params.id!)
2020
/// }
2121
/// Route {

0 commit comments

Comments
 (0)