Skip to content

Commit afa0ca9

Browse files
Case study for list of navigation links. (pointfreeco#6)
* Case study for list of navigation links. * renames * wip Co-authored-by: Stephen Celis <[email protected]>
1 parent 7d39688 commit afa0ca9

File tree

6 files changed

+161
-9
lines changed

6 files changed

+161
-9
lines changed

Examples/CaseStudies/06-NavigationLinks.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ struct OptionalNavigationLinks: View {
1313
NavigationLink(
1414
unwrapping: self.$viewModel.fact,
1515
destination: { $fact in
16-
let _ = print(fact)
1716
FactEditor(fact: $fact.description)
1817
.disabled(self.viewModel.isLoading)
1918
.foregroundColor(self.viewModel.isLoading ? .gray : nil)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import SwiftUI
2+
import SwiftUINavigation
3+
4+
private let readMe = """
5+
This case study demonstrates how to model a list of navigation links. Tap a row to drill down \
6+
and edit a counter. Edit screen allows cancelling or saving the edits.
7+
8+
The domain for a row in the list has its own ObservableObject and Route enum, and it uses the \
9+
library's NavigationLink initializer to drive navigation from the route enum.
10+
"""
11+
12+
struct ListOfNavigationLinks: View {
13+
@ObservedObject var viewModel: ListOfNavigationLinksViewModel
14+
15+
var body: some View {
16+
Form {
17+
Section {
18+
Text(readMe)
19+
}
20+
21+
List {
22+
ForEach(self.viewModel.rows) { rowViewModel in
23+
RowView(viewModel: rowViewModel)
24+
}
25+
.onDelete(perform: self.viewModel.deleteButtonTapped(indexSet:))
26+
}
27+
}
28+
.navigationTitle("List of Links")
29+
.toolbar {
30+
ToolbarItem {
31+
Button("Add") {
32+
self.viewModel.addButtonTapped()
33+
}
34+
}
35+
}
36+
}
37+
}
38+
39+
class ListOfNavigationLinksViewModel: ObservableObject {
40+
@Published var rows: [ListOfNavigationLinksRowViewModel]
41+
42+
init(rows: [ListOfNavigationLinksRowViewModel] = []) {
43+
self.rows = rows
44+
}
45+
46+
func addButtonTapped() {
47+
withAnimation {
48+
self.rows.append(.init())
49+
}
50+
}
51+
52+
func deleteButtonTapped(indexSet: IndexSet) {
53+
self.rows.remove(atOffsets: indexSet)
54+
}
55+
}
56+
57+
fileprivate struct RowView: View {
58+
@ObservedObject var viewModel: ListOfNavigationLinksRowViewModel
59+
60+
var body: some View {
61+
NavigationLink(
62+
unwrapping: self.$viewModel.route,
63+
case: /ListOfNavigationLinksRowViewModel.Route.edit
64+
) { $counter in
65+
EditView(counter: $counter)
66+
.navigationBarBackButtonHidden(true)
67+
.toolbar {
68+
ToolbarItem(placement: .primaryAction) {
69+
Button("Save") { self.viewModel.saveButtonTapped(counter: counter) }
70+
}
71+
ToolbarItem(placement: .cancellationAction) {
72+
Button("Cancel") { self.viewModel.cancelButtonTapped() }
73+
}
74+
}
75+
} onNavigate: {
76+
self.viewModel.setEditNavigation(isActive: $0)
77+
} label: {
78+
Text("\(self.viewModel.counter)")
79+
}
80+
}
81+
}
82+
83+
class ListOfNavigationLinksRowViewModel: Identifiable, ObservableObject {
84+
let id = UUID()
85+
@Published var counter: Int
86+
@Published var route: Route?
87+
88+
enum Route {
89+
case edit(Int)
90+
}
91+
92+
init(
93+
counter: Int = 0,
94+
route: Route? = nil
95+
) {
96+
self.counter = counter
97+
self.route = route
98+
}
99+
100+
func setEditNavigation(isActive: Bool) {
101+
self.route = isActive ? .edit(self.counter) : nil
102+
}
103+
104+
func saveButtonTapped(counter: Int) {
105+
self.counter = counter
106+
self.route = nil
107+
}
108+
109+
func cancelButtonTapped() {
110+
self.route = nil
111+
}
112+
}
113+
114+
fileprivate struct EditView: View {
115+
@Binding var counter: Int
116+
117+
var body: some View {
118+
Form {
119+
Text("Count: \(self.counter)")
120+
Button("Increment") {
121+
self.counter += 1
122+
}
123+
Button("Decrement") {
124+
self.counter -= 1
125+
}
126+
}
127+
}
128+
}
129+
130+
struct ListOfNavigationLinks_Previews: PreviewProvider {
131+
static var previews: some View {
132+
NavigationView {
133+
ListOfNavigationLinks(
134+
viewModel: .init(
135+
rows: [
136+
.init(counter: 0),
137+
.init(counter: 0),
138+
.init(counter: 0),
139+
.init(counter: 0),
140+
.init(counter: 0),
141+
]
142+
)
143+
)
144+
}
145+
}
146+
}

Examples/CaseStudies/RootView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ struct RootView: View {
3535
NavigationLink("Optional navigation links") {
3636
OptionalNavigationLinks()
3737
}
38+
NavigationLink("List of navigation links") {
39+
ListOfNavigationLinks(viewModel: .init())
40+
}
3841
} header: {
3942
Text("Navigation links")
4043
}

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
CA473838272F0D860012CAC3 /* 03-Sheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA473832272F0D860012CAC3 /* 03-Sheets.swift */; };
2323
CA473839272F0D860012CAC3 /* 01-Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA473833272F0D860012CAC3 /* 01-Alerts.swift */; };
2424
CA47383B272F0DD60012CAC3 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA47383A272F0DD60012CAC3 /* SwiftUINavigation */; };
25-
CA47383E272F0F9B0012CAC3 /* 08-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 08-CustomComponents.swift */; };
26-
CABE9FC1272F2C0000AFC150 /* 07-Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABE9FC0272F2C0000AFC150 /* 07-Routing.swift */; };
25+
CA47383E272F0F9B0012CAC3 /* 09-CustomComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */; };
26+
CA70FED7274B1907005A0D53 /* 07-NavigationLinkList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */; };
27+
CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */; };
2728
DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */; };
2829
DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */; };
2930
DCD4E68B274180F500CDF3BD /* 06-NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */; };
@@ -46,8 +47,9 @@
4647
CA473832272F0D860012CAC3 /* 03-Sheets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "03-Sheets.swift"; sourceTree = "<group>"; };
4748
CA473833272F0D860012CAC3 /* 01-Alerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "01-Alerts.swift"; sourceTree = "<group>"; };
4849
CA47383C272F0F0D0012CAC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
49-
CA47383D272F0F9B0012CAC3 /* 08-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-CustomComponents.swift"; sourceTree = "<group>"; };
50-
CABE9FC0272F2C0000AFC150 /* 07-Routing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "07-Routing.swift"; sourceTree = "<group>"; };
50+
CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "09-CustomComponents.swift"; sourceTree = "<group>"; };
51+
CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "07-NavigationLinkList.swift"; sourceTree = "<group>"; };
52+
CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-Routing.swift"; sourceTree = "<group>"; };
5153
DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-FullScreenCovers.swift"; sourceTree = "<group>"; };
5254
DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-Popovers.swift"; sourceTree = "<group>"; };
5355
DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "06-NavigationLinks.swift"; sourceTree = "<group>"; };
@@ -123,8 +125,9 @@
123125
DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */,
124126
DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */,
125127
DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */,
126-
CABE9FC0272F2C0000AFC150 /* 07-Routing.swift */,
127-
CA47383D272F0F9B0012CAC3 /* 08-CustomComponents.swift */,
128+
CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */,
129+
CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */,
130+
CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */,
128131
CA473830272F0D860012CAC3 /* CaseStudiesApp.swift */,
129132
CA473831272F0D860012CAC3 /* FactClient.swift */,
130133
CA47382E272F0D860012CAC3 /* RootView.swift */,
@@ -254,10 +257,11 @@
254257
isa = PBXSourcesBuildPhase;
255258
buildActionMask = 2147483647;
256259
files = (
257-
CABE9FC1272F2C0000AFC150 /* 07-Routing.swift in Sources */,
260+
CABE9FC1272F2C0000AFC150 /* 08-Routing.swift in Sources */,
258261
CA473837272F0D860012CAC3 /* FactClient.swift in Sources */,
259262
CA473835272F0D860012CAC3 /* 02-ConfirmationDialogs.swift in Sources */,
260-
CA47383E272F0F9B0012CAC3 /* 08-CustomComponents.swift in Sources */,
263+
CA47383E272F0F9B0012CAC3 /* 09-CustomComponents.swift in Sources */,
264+
CA70FED7274B1907005A0D53 /* 07-NavigationLinkList.swift in Sources */,
261265
CA473836272F0D860012CAC3 /* CaseStudiesApp.swift in Sources */,
262266
DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */,
263267
DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */,

0 commit comments

Comments
 (0)