Skip to content

Commit 54c5aac

Browse files
authored
RouteStack; Example project (#5)
1 parent 3ef8018 commit 54c5aac

File tree

13 files changed

+154
-137
lines changed

13 files changed

+154
-137
lines changed

Examples/MusicApp/MusicApp.xcodeproj/project.pbxproj

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// !$*UTF8*$!
22
{
33
archiveVersion = 1;
4-
classes = {};
4+
classes = {
5+
};
56
objectVersion = 60;
67
objects = {
78

89
/* Begin PBXBuildFile section */
10+
00F367692EA1FFDF00AD315E /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F367682EA1FFDD00AD315E /* Routes.swift */; };
911
84A968752C7E34EE00AA1E20 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A968752C7E34EE00AA1E10 /* Album.swift */; };
1012
84A968752C7E34EE00AA1E21 /* AlbumDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A968752C7E34EE00AA1E11 /* AlbumDetailView.swift */; };
1113
84A968752C7E34EE00AA1E22 /* AlbumListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A968752C7E34EE00AA1E12 /* AlbumListView.swift */; };
@@ -15,6 +17,7 @@
1517
/* End PBXBuildFile section */
1618

1719
/* Begin PBXFileReference section */
20+
00F367682EA1FFDD00AD315E /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = "<group>"; };
1821
84A968752C7E34EE00AA1E10 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
1922
84A968752C7E34EE00AA1E11 /* AlbumDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailView.swift; sourceTree = "<group>"; };
2023
84A968752C7E34EE00AA1E12 /* AlbumListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListView.swift; sourceTree = "<group>"; };
@@ -55,6 +58,7 @@
5558
84A968752C7E34EE00AA1E08 /* MusicApp */ = {
5659
isa = PBXGroup;
5760
children = (
61+
00F367682EA1FFDD00AD315E /* Routes.swift */,
5862
84A968752C7E34EE00AA1E13 /* MusicApp.swift */,
5963
84A968752C7E34EE00AA1E10 /* Album.swift */,
6064
84A968752C7E34EE00AA1E11 /* AlbumDetailView.swift */,
@@ -137,6 +141,7 @@
137141
files = (
138142
84A968752C7E34EE00AA1E23 /* MusicApp.swift in Sources */,
139143
84A968752C7E34EE00AA1E20 /* Album.swift in Sources */,
144+
00F367692EA1FFDF00AD315E /* Routes.swift in Sources */,
140145
84A968752C7E34EE00AA1E21 /* AlbumDetailView.swift in Sources */,
141146
84A968752C7E34EE00AA1E22 /* AlbumListView.swift in Sources */,
142147
);
@@ -258,7 +263,6 @@
258263
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
259264
CODE_SIGN_STYLE = Automatic;
260265
CURRENT_PROJECT_VERSION = 1;
261-
DEVELOPMENT_ASSET_PATHS = "MusicApp/Preview Content";
262266
DEVELOPMENT_TEAM = "";
263267
ENABLE_PREVIEWS = YES;
264268
GENERATE_INFOPLIST_FILE = NO;
@@ -268,7 +272,10 @@
268272
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
269273
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
270274
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
271-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
275+
LD_RUNPATH_SEARCH_PATHS = (
276+
"$(inherited)",
277+
"@executable_path/Frameworks",
278+
);
272279
MARKETING_VERSION = 1.0;
273280
PRODUCT_BUNDLE_IDENTIFIER = com.example.MusicApp;
274281
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -286,7 +293,6 @@
286293
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
287294
CODE_SIGN_STYLE = Automatic;
288295
CURRENT_PROJECT_VERSION = 1;
289-
DEVELOPMENT_ASSET_PATHS = "MusicApp/Preview Content";
290296
DEVELOPMENT_TEAM = "";
291297
ENABLE_PREVIEWS = YES;
292298
GENERATE_INFOPLIST_FILE = NO;
@@ -296,7 +302,10 @@
296302
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
297303
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
298304
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
299-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
305+
LD_RUNPATH_SEARCH_PATHS = (
306+
"$(inherited)",
307+
"@executable_path/Frameworks",
308+
);
300309
MARKETING_VERSION = 1.0;
301310
PRODUCT_BUNDLE_IDENTIFIER = com.example.MusicApp;
302311
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -333,7 +342,7 @@
333342
/* Begin XCLocalSwiftPackageReference section */
334343
84A968752C7E34EE00AA1E40 /* XCLocalSwiftPackageReference "../../" */ = {
335344
isa = XCLocalSwiftPackageReference;
336-
relativePath = "../../";
345+
relativePath = ../../;
337346
};
338347
/* End XCLocalSwiftPackageReference section */
339348

@@ -344,7 +353,6 @@
344353
productName = SwiftUIRoutes;
345354
};
346355
/* End XCSwiftPackageProductDependency section */
347-
348356
};
349357
rootObject = 84A968752C7E34EE00AA1E01 /* Project object */;
350358
}

Examples/MusicApp/MusicApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/MusicApp/MusicApp/Album.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ enum AlbumStore {
4343
)
4444
]
4545

46-
static func album(withID id: String) -> Album? {
46+
static func album(id: String) -> Album? {
4747
library.first { $0.id == id }
4848
}
4949
}

Examples/MusicApp/MusicApp/AlbumDetailView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import SwiftUIRoutes
44
struct AlbumDetailView: View {
55
@Environment(\.dismiss) private var dismiss
66
@Environment(\.routePath) private var routePath
7-
@Environment(\.routeSheet) private var routeSheet
7+
8+
@State var sheet: Routable?
89

910
let album: Album
1011

@@ -72,10 +73,11 @@ struct AlbumDetailView: View {
7273
.toolbar {
7374
ToolbarItem(placement: .navigationBarTrailing) {
7475
Button("Preview") {
75-
routeSheet.wrappedValue = album
76+
sheet = album
7677
}
7778
}
7879
}
80+
.routeSheetStack(routes: routes, item: $sheet)
7981
}
8082
}
8183

Examples/MusicApp/MusicApp/AlbumListView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import SwiftUIRoutes
33

44
struct AlbumListView: View {
55
@Environment(\.routePath) private var routePath
6-
@Environment(\.routeSheet) private var routeSheet
76

87
private let albums = AlbumStore.library
98

9+
@State var sheet: Routable?
10+
1011
var body: some View {
1112
List {
1213
Section("Library") {
@@ -33,7 +34,7 @@ struct AlbumListView: View {
3334

3435
Button {
3536
if let second = albums.dropFirst().first {
36-
routeSheet.wrappedValue = second
37+
sheet = second
3738
}
3839
} label: {
3940
Label("Preview second album", systemImage: "rectangle.portrait.and.arrow.right")
@@ -55,11 +56,12 @@ struct AlbumListView: View {
5556
}
5657
}
5758
.navigationTitle("Albums")
59+
.routeSheet(routes: routes, item: $sheet, stacked: true)
5860
}
5961
}
6062

6163
#Preview {
62-
NavigationStack {
64+
RouteStack(routes: routes) {
6365
AlbumListView()
6466
}
6567
}

Examples/MusicApp/MusicApp/MusicApp.swift

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,19 @@ import SwiftUI
22
import SwiftUIRoutes
33

44
@main
5-
struct MusicApp: App {
6-
private let routes: Routes
5+
struct MusicApp: App {
76
@State private var path = RoutePath()
87
@State private var sheet: Routable?
98

10-
init() {
11-
let routes = Routes()
12-
Self.register(routes)
13-
self.routes = routes
14-
}
9+
init() {}
1510

1611
var body: some Scene {
1712
WindowGroup {
1813
NavigationStack(path: $path) {
1914
AlbumListView()
2015
.routesDestination(routes: routes, path: $path)
21-
.routesSheet(routes: routes, item: $sheet, path: $path)
2216
}
17+
.routeSheet(routes: routes, item: $sheet)
2318
.onOpenURL(perform: handleDeepLink(_:))
2419
}
2520
}
@@ -36,24 +31,10 @@ struct MusicApp: App {
3631
path = RoutePath()
3732
path.push(route)
3833
}
39-
40-
private static func register(_ routes: Routes) {
41-
routes.register(path: "/album/:id") { route in
42-
if let id = route.param("id"), let album = AlbumStore.album(withID: id) {
43-
AlbumDetailView(album: album)
44-
} else {
45-
AlbumUnavailableView(missingID: route.param("id") ?? route.path)
46-
}
47-
}
48-
49-
routes.register(type: Album.self) { album in
50-
AlbumDetailView(album: album)
51-
}
52-
}
5334
}
5435

55-
private struct AlbumUnavailableView: View {
56-
let missingID: String
36+
struct AlbumUnavailableView: View {
37+
let id: String
5738

5839
var body: some View {
5940
VStack(spacing: 16) {
@@ -62,7 +43,7 @@ private struct AlbumUnavailableView: View {
6243
.foregroundStyle(.orange)
6344
Text("Album not found")
6445
.font(.headline)
65-
Text("We couldn't find an album with id \(missingID).")
46+
Text("We couldn't find an album with id \(id).")
6647
.multilineTextAlignment(.center)
6748
.foregroundStyle(.secondary)
6849
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import SwiftUIRoutes
2+
3+
@MainActor
4+
var routes: Routes {
5+
let routes = Routes()
6+
register(routes: routes)
7+
return routes
8+
}
9+
10+
@MainActor
11+
private func register(routes: Routes) {
12+
routes.register(path: "/album/:id") { route in
13+
if let id = route.param("id"), let album = AlbumStore.album(id: id) {
14+
AlbumDetailView(album: album)
15+
} else {
16+
AlbumUnavailableView(id: route.param("id") ?? route.path)
17+
}
18+
}
19+
20+
routes.register(type: Album.self) { album in
21+
AlbumDetailView(album: album)
22+
}
23+
}

README.md

Lines changed: 31 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,36 @@
22

33
SwiftUI Routes centralizes navigation destinations so you can describe navigation by path strings or strongly typed values.
44

5+
## Example Project
6+
7+
Explore `Examples/MusicApp` for a complete sample integrating SwiftUI Routes; open `Examples/MusicApp/MusicApp.xcodeproj` in Xcode to run it.
8+
59
## Register
610

7-
Start by creating a `Routes` instance and registering destinations. Registrations accept either a resource path (string) or a `Routable` value. Paths can be parameterized to include params (like `id`).
11+
Start by creating a `Routes.swift` file and registering destinations. Registrations accept either a resource path (string) or a `Routable` value. Paths can be parameterized to include params (like `id`).
812

913
```swift
10-
let routes = Routes()
14+
import SwiftUIRoutes
1115

12-
routes.register(path: "/album/:id") { route in
13-
AlbumView(id: route.param("id") ?? "unknown")
16+
@MainActor
17+
var routes: Routes {
18+
let routes = Routes()
19+
register(routes: routes)
20+
return routes
1421
}
1522

16-
routes.register(type: Album.self) { album in
17-
AlbumDetailView(album: album)
23+
@MainActor
24+
private func register(routes: Routes) {
25+
routes.register(path: "/album/:id") { route in
26+
if let id = route.param("id") {
27+
AlbumView(id: id)
28+
}
29+
}
30+
31+
routes.register(type: Album.self) { album in
32+
AlbumDetailView(album: album)
33+
}
1834
}
19-
```
2035

2136
- Path registrations use URL-style patterns. The closure receives a `Route` so you can pull out parameters or query items with `route.param(_:)` or `route.params`.
2237
- Type registrations work with any `Routable`. Conforming types define how to turn a value into the resource path that should be presented.
@@ -52,7 +67,7 @@ struct LookupExample: View {
5267

5368
## NavigationStack
5469

55-
Attach your routes to a `NavigationStack` by keeping a `RoutePath` binding. The modifier installs every registered destination and exposes the binding through `EnvironmentValues.routePath`.
70+
Attach your routes to a `NavigationStack` by keeping a `RoutePath` binding. The modifier installs every registered destination and exposes the binding through `EnvironmentValues.routePath`. Define `routesDestination` on the root view.
5671

5772
```swift
5873
struct AppScene: View {
@@ -70,17 +85,6 @@ struct AppScene: View {
7085
}
7186
}
7287
}
73-
74-
// Expose routes registration in your package (optional)
75-
public func register(routes: Routes) {
76-
routes.register(path: "/album/:id") { route in
77-
AlbumView(id: route.param("id") ?? "unknown")
78-
}
79-
80-
routes.register(type: Album.self) { album in
81-
AlbumDetailView(album: album)
82-
}
83-
}
8488
```
8589

8690
Views inside the stack can push routes directly or use the provided view modifiers.
@@ -110,53 +114,22 @@ The `push(_:style:)` modifier wraps any view in a navigation trigger while still
110114

111115
## Sheets
112116

113-
Reuse the same routes for modal sheets by keeping a `Routable?` binding and attaching `routesSheet`.
114-
115-
```swift
116-
struct ContentView: View {
117-
private let routes = Routes()
118-
@State private var path = RoutePath()
119-
@State private var sheet: Routable?
120-
121-
init() {
122-
register(routes: routes)
123-
}
124-
125-
var body: some View {
126-
NavigationStack(path: $path) {
127-
HomeView()
128-
.routesDestination(routes: routes, path: $path)
129-
.routesSheet(routes: routes, item: $sheet, path: $path)
130-
}
131-
}
132-
}
133-
134-
func register(routes: Routes) {
135-
routes.register(path: "/album/:id") { route in
136-
AlbumView(id: route.param("id") ?? "unknown")
137-
}
138-
139-
routes.register(type: Album.self) { album in
140-
AlbumDetailView(album: album)
141-
}
142-
}
143-
```
144-
145-
When `routesSheet` is present you can present any registered destination with the same APIs used for stacks.
117+
Define a sheet binding and use `routeSheet`. If `stacked` is `true`, it will wrap the route view in another NavigationStack in case those views push.
146118

147119
```swift
148120
struct HomeView: View {
149-
@Environment(\.routeSheet) private var sheet
121+
@State private var sheet: Routable?
122+
123+
let album: Album
150124

151125
var body: some View {
152126
VStack(spacing: 24) {
153-
Button("Preview Album") {
154-
sheet.wrappedValue = Route("/album/123")
127+
Button("Open Album") {
128+
sheet = album
155129
}
156-
157-
Text("Show Album")
158-
.sheet(Album(id: "123"))
159130
}
131+
// If stacked is true will wrap in a new NavigationStack configured with these routes
132+
.routeSheet(routes: routes, item: $sheet, stacked: true)
160133
}
161134
}
162135
```

0 commit comments

Comments
 (0)