From 9724d222355ac808ab6af9c88a0a3e8f2213caf4 Mon Sep 17 00:00:00 2001 From: Daniel Haight Date: Wed, 10 Nov 2021 01:25:40 +0000 Subject: [PATCH] Add `NavigationLinks` and `Bindings` This gives us a more readable approach to set up navigation links that mirrors other `SwiftUI` navigation paradigms such as `.sheet`. We use the modifier to simplify navigation link setup for `TransactionHistoryView` We also include some `Binding` extensions to help creating them. --- secant.xcodeproj/project.pbxproj | 8 ++ .../Views/TransactionHistoryView.swift | 10 +-- secant/Util/Bindings.swift | 88 +++++++++++++++++++ secant/Util/NavigationLinks.swift | 27 ++++++ 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 secant/Util/Bindings.swift create mode 100644 secant/Util/NavigationLinks.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 18711dc..b6f632c 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -89,10 +89,12 @@ 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */; }; 66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */; }; 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */; }; + F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9322DBF273B555C00C105B5 /* NavigationLinks.swift */; }; F96B41E7273B501F0021B49A /* TransactionHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */; }; F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41E5273B501F0021B49A /* TransactionDetailView.swift */; }; F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */; }; F96B41EB273B50520021B49A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41EA273B50520021B49A /* Strings.swift */; }; + F9C165B4274031F600592F76 /* Bindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165B3274031F600592F76 /* Bindings.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -200,10 +202,12 @@ 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = ""; }; 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonStyle.swift; sourceTree = ""; }; 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = ""; }; + F9322DBF273B555C00C105B5 /* NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationLinks.swift; sourceTree = ""; }; F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryStore.swift; sourceTree = ""; }; F96B41E5273B501F0021B49A /* TransactionDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = ""; }; F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryView.swift; sourceTree = ""; }; F96B41EA273B50520021B49A /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + F9C165B3274031F600592F76 /* Bindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bindings.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -534,6 +538,8 @@ 0DACFA7E27208CE00039EEA5 /* Clamped.swift */, 0DACFA8027208D940039EEA5 /* UInt+SuperscriptText.swift */, F96B41EA273B50520021B49A /* Strings.swift */, + F9322DBF273B555C00C105B5 /* NavigationLinks.swift */, + F9C165B3274031F600592F76 /* Bindings.swift */, ); path = Util; sourceTree = ""; @@ -887,6 +893,7 @@ 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */, 665C963F272C26E600BC04FB /* CircularFrameBackground.swift in Sources */, 0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */, + F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */, 0D32282326C586A800262533 /* HistoryScreen.swift in Sources */, 0D864A0A26E154FD00A61879 /* InitFailedScreenViewModel.swift in Sources */, 0DA13CA526C1963000E3B610 /* Balance.swift in Sources */, @@ -916,6 +923,7 @@ 663FAB9E271D875700E495F8 /* CreateButton.swift in Sources */, 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */, 0DA13C8F26C15D1D00E3B610 /* WelcomeScreen.swift in Sources */, + F9C165B4274031F600592F76 /* Bindings.swift in Sources */, 0D32282826C586E000262533 /* RequestZcashScreen.swift in Sources */, F96B41EB273B50520021B49A /* Strings.swift in Sources */, 0D32283226C5877A00262533 /* BalanceScreen.swift in Sources */, diff --git a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift index 96c4aed..0c4d868 100644 --- a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift +++ b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift @@ -8,11 +8,11 @@ struct TransactionHistoryView: View { WithViewStore(store) { viewStore in List { ForEach(viewStore.transactions) { transaction in - NavigationLink( - isActive: viewStore.bindingForSelectingTransaction(transaction), - destination: { TransactionDetailView(transaction: transaction) }, - label: { Text("Show Transaction \(transaction.id)") } - ) + Text("Show Transaction \(transaction.id)") + .navigationLink( + isActive: viewStore.bindingForSelectingTransaction(transaction), + destination: { TransactionDetailView(transaction: transaction) } + ) } } .navigationTitle(Text("Transactions")) diff --git a/secant/Util/Bindings.swift b/secant/Util/Bindings.swift new file mode 100644 index 0000000..a72b88b --- /dev/null +++ b/secant/Util/Bindings.swift @@ -0,0 +1,88 @@ +import SwiftUI +import CasePaths + +/// taken largely from: https://github.com/pointfreeco/episode-code-samples/blob/main/0167-navigation-pt8/SwiftUINavigation/SwiftUINavigation/SwiftUIHelpers.swift +extension Binding { + func isPresent() -> Binding + where Value == Wrapped? { + .init( + get: { self.wrappedValue != nil }, + set: { isPresented in + if !isPresented { + self.wrappedValue = nil + } + } + ) + } + + func isPresent(_ casePath: CasePath) -> Binding + where Value == Enum? { + Binding( + get: { + if let wrappedValue = self.wrappedValue, casePath.extract(from: wrappedValue) != nil { + return true + } else { + return false + } + }, + set: { isPresented in + if !isPresented { + self.wrappedValue = nil + } + } + ) + } + + func `case`(_ casePath: CasePath) -> Binding + where Value == Enum? { + Binding( + get: { + guard + let wrappedValue = self.wrappedValue, + let `case` = casePath.extract(from: wrappedValue) + else { return nil } + return `case` + }, + // swiftlint:disable:next unused_closure_parameter + set: { `case` in + if let `case` = `case` { + self.wrappedValue = casePath.embed(`case`) + } else { + self.wrappedValue = nil + } + } + ) + } + + func didSet(_ callback: @escaping (Value) -> Void) -> Self { + .init( + get: { self.wrappedValue }, + set: { + self.wrappedValue = $0 + callback($0) + } + ) + } + + init?(unwrap binding: Binding) { + guard let wrappedValue = binding.wrappedValue + else { return nil } + + self.init( + get: { wrappedValue }, + set: { binding.wrappedValue = $0 } + ) + } + + func map(extract: @escaping (Value) -> T, embed: @escaping (T) -> Value?) -> Binding { + Binding( + get: { extract(wrappedValue) }, + set: { + guard let value = embed($0) else { + return + } + wrappedValue = value + } + ) + } +} diff --git a/secant/Util/NavigationLinks.swift b/secant/Util/NavigationLinks.swift new file mode 100644 index 0000000..95b0216 --- /dev/null +++ b/secant/Util/NavigationLinks.swift @@ -0,0 +1,27 @@ +import SwiftUI + +extension View { + func navigationLink( + isActive: Binding, + destination: @escaping () -> Destination + ) -> some View { + NavigationLink( + isActive: isActive, + destination: destination, + label: { self } + ) + } + + func navigationLinkEmpty( + isActive: Binding, + destination: @escaping () -> Destination + ) -> some View { + return self.overlay( + NavigationLink( + isActive: isActive, + destination: destination, + label: { EmptyView() } + ) + ) + } +}