Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SwiftUI's View.navigate() + CaseStudy #20

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 83 additions & 0 deletions Examples/CaseStudies/10-FullProgrammaticNavigation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import SwiftUINavigation

private let readMe = """
This case study demonstrates how to progammaticaly navigate in SwiftUI. All your components can be isolated from navigation, just using a basic ViewModifier on your views for navigation.
"""

// ========================================================================
// Navigation routes
// ========================================================================

enum ARoute: Equatable {
case b(BRoute?)
}
enum BRoute: Equatable {
case c(CRoute?)
}
enum CRoute: Equatable {
case d
}

// ========================================================================
// Main navigation
// ========================================================================

struct FullProgrammaticNavigation: View {
@State var route: ARoute?

var body: some View {
A()
.onTapGesture { route = .b(.none) }
.navigate(when: $route, is: /ARoute.b) { _ in
theBView
}
}

var theBView: some View {
B()
.onTapGesture { route = .b(.c(.none)) }
.navigate(when: $route.case(in: /ARoute.b), is: /BRoute.c) { _ in
theCView
}
}

var theCView: some View {
C()
.onTapGesture { route = .b(.c(.d)) }
.navigate(when: $route.case(in: /ARoute.b).case(in: /BRoute.c), is: /CRoute.d) { _ in
theDView
}
}

var theDView: some View {
D()
}
}

// ========================================================================
// Independent A/B/C views
// ========================================================================

struct A: View {
var body: some View {
Text("A view - Tap to navigate to B")
}
}

struct B: View {
var body: some View {
Text("B view - Tap to navigate to C")
}
}

struct C: View {
var body: some View {
Text("C view - Tap to navigate to D")
}
}

struct D: View {
var body: some View {
Text("D view - No navigation")
}
}
3 changes: 3 additions & 0 deletions Examples/CaseStudies/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ struct RootView: View {
NavigationLink("List of navigation links") {
ListOfNavigationLinks(viewModel: .init())
}
NavigationLink("Full programmatic navigation") {
FullProgrammaticNavigation()
}
} header: {
Text("Navigation links")
}
Expand Down
4 changes: 4 additions & 0 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */; };
DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */; };
DCD4E68B274180F500CDF3BD /* 06-NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */; };
F88630DA28488AC4004D6252 /* 10-FullProgrammaticNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88630D928488AC4004D6252 /* 10-FullProgrammaticNavigation.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -53,6 +54,7 @@
DCD4E684273B300F00CDF3BD /* 05-FullScreenCovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-FullScreenCovers.swift"; sourceTree = "<group>"; };
DCD4E686273B30DA00CDF3BD /* 04-Popovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-Popovers.swift"; sourceTree = "<group>"; };
DCD4E68A274180F500CDF3BD /* 06-NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "06-NavigationLinks.swift"; sourceTree = "<group>"; };
F88630D928488AC4004D6252 /* 10-FullProgrammaticNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "10-FullProgrammaticNavigation.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -128,6 +130,7 @@
CA70FED6274B1907005A0D53 /* 07-NavigationLinkList.swift */,
CABE9FC0272F2C0000AFC150 /* 08-Routing.swift */,
CA47383D272F0F9B0012CAC3 /* 09-CustomComponents.swift */,
F88630D928488AC4004D6252 /* 10-FullProgrammaticNavigation.swift */,
CA473830272F0D860012CAC3 /* CaseStudiesApp.swift */,
CA473831272F0D860012CAC3 /* FactClient.swift */,
CA47382E272F0D860012CAC3 /* RootView.swift */,
Expand Down Expand Up @@ -266,6 +269,7 @@
DCD4E687273B30DA00CDF3BD /* 04-Popovers.swift in Sources */,
DCD4E685273B300F00CDF3BD /* 05-FullScreenCovers.swift in Sources */,
CA473834272F0D860012CAC3 /* RootView.swift in Sources */,
F88630DA28488AC4004D6252 /* 10-FullProgrammaticNavigation.swift in Sources */,
CA473839272F0D860012CAC3 /* 01-Alerts.swift in Sources */,
DCD4E68B274180F500CDF3BD /* 06-NavigationLinks.swift in Sources */,
CA473838272F0D860012CAC3 /* 03-Sheets.swift in Sources */,
Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftUINavigation/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ extension Binding {
)
}

/// Creates a binding by projecting the current enum value to the value at a particular
/// case.
///
/// > Note: This method is constrained to optionals so that the projected value can write `nil`
/// > back to the parent, which is useful for navigation, particularly dismissal.
///
/// - Parameter casePath: A case path that identifies a particular case to unwrap.
/// - Returns: A binding to an enum case.
public func `case`<Case>(in casePath: CasePath<Value, Case?>) -> Binding<Case?> {
.init(
get: {
guard let value = casePath.extract(from: wrappedValue) else {
return .none
}
return value
},
set: { newValue in
wrappedValue = casePath.embed(newValue)
}
)
}

/// Creates a binding by projecting the current optional value to a boolean describing if it's
/// non-`nil`.
///
Expand Down
37 changes: 37 additions & 0 deletions Sources/SwiftUINavigation/Navigate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
extension View {
public func navigate<DestinationContent: View, Enum, Case>(
when unwrapping: Binding<Enum?>,
is casePath: CasePath<Enum, Case>,
onNavigate: ((Bool) -> Void)? = nil,
to destination: @escaping (Binding<Case>) -> DestinationContent
) -> some View {
modifier(
NavigateViewModifier(
unwrapping: unwrapping,
casePath: casePath,
onNavigate: onNavigate,
destinationContent: destination
)
)
}
}

private struct NavigateViewModifier<DestinationContent: View, Enum, Case>: ViewModifier {
let unwrapping: Binding<Enum?>
let casePath: CasePath<Enum, Case>
let onNavigate: ((Bool) -> Void)?
let destinationContent: (Binding<Case>) -> DestinationContent

func body(content: Content) -> some View {
VStack(spacing: .zero) {
NavigationLink(
unwrapping: unwrapping,
case: casePath,
destination: destinationContent,
onNavigate: onNavigate ?? { _ in },
label: { EmptyView() }
)
content
}
}
}