From c6a98f06c28cb5893ddf9dbcf858b484e819fe5b Mon Sep 17 00:00:00 2001 From: Daniel Haight Date: Mon, 15 Nov 2021 16:43:47 +0000 Subject: [PATCH] Add `WithStateBinding` This is a way to add back some of the "free" animations that we lose when creating custom bindings instead of using `@State` variables, which is extremely common in TCA / unidirectional architecture. For an example check out the live preview in the `WithStateBinding` file. The first is with a standard State binding, click to get the detail and then drag from the leading edge slowly. You will notice the selection highlight _gradually_ gets less the more you drag (or stronger if you go back). For the second one with a custom binding, drag back and you will notice it is either entirely selected, or deselected. The third uses `WithStateBinding` to give _back_ the nice animations to custom bindings. --- secant.xcodeproj/project.pbxproj | 8 +++ .../Views/TransactionHistoryView.swift | 12 ++-- secant/Util/Previews.swift | 17 ++++++ secant/Util/WithStateBinding.swift | 59 +++++++++++++++++++ 4 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 secant/Util/Previews.swift create mode 100644 secant/Util/WithStateBinding.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index fb66721..494fe1b 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 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 */; }; + F93673D62742CB840099C6AF /* Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93673D52742CB840099C6AF /* Previews.swift */; }; F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874ED273C4DE200F0E875 /* HomeStore.swift */; }; F93874F1273C4DE200F0E875 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874EF273C4DE200F0E875 /* HomeView.swift */; }; F96B41E7273B501F0021B49A /* TransactionHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */; }; @@ -97,6 +98,7 @@ 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 */; }; + F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -205,6 +207,7 @@ 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 = ""; }; + F93673D52742CB840099C6AF /* Previews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = ""; }; F93874ED273C4DE200F0E875 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; F93874EF273C4DE200F0E875 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryStore.swift; sourceTree = ""; }; @@ -212,6 +215,7 @@ 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 = ""; }; + F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithStateBinding.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -544,6 +548,8 @@ F96B41EA273B50520021B49A /* Strings.swift */, F9322DBF273B555C00C105B5 /* NavigationLinks.swift */, F9C165B3274031F600592F76 /* Bindings.swift */, + F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */, + F93673D52742CB840099C6AF /* Previews.swift */, ); path = Util; sourceTree = ""; @@ -911,6 +917,8 @@ 0D864A0E26E1583000A61879 /* LoadingScreen.swift in Sources */, 0DA13C9C26C1942100E3B610 /* BackupWalletScreen.swift in Sources */, 0DA13C9826C186FF00E3B610 /* RestoreWalletScreenViewModel.swift in Sources */, + F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */, + F93673D62742CB840099C6AF /* Previews.swift in Sources */, 0D32283326C5877A00262533 /* BalanceScreenViewModel.swift in Sources */, 0D5D16F526E24CCF00AD33D1 /* AppError.swift in Sources */, 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */, diff --git a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift index 0c4d868..cd88127 100644 --- a/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift +++ b/secant/Features/TransactionHistory/Views/TransactionHistoryView.swift @@ -8,11 +8,13 @@ struct TransactionHistoryView: View { WithViewStore(store) { viewStore in List { ForEach(viewStore.transactions) { transaction in - Text("Show Transaction \(transaction.id)") - .navigationLink( - isActive: viewStore.bindingForSelectingTransaction(transaction), - destination: { TransactionDetailView(transaction: transaction) } - ) + WithStateBinding(binding: viewStore.bindingForSelectingTransaction(transaction)) { + Text("Show Transaction \(transaction.id)") + .navigationLink( + isActive: $0, + destination: { TransactionDetailView(transaction: transaction) } + ) + } } } .navigationTitle(Text("Transactions")) diff --git a/secant/Util/Previews.swift b/secant/Util/Previews.swift new file mode 100644 index 0000000..30bd4f9 --- /dev/null +++ b/secant/Util/Previews.swift @@ -0,0 +1,17 @@ +import SwiftUI + +#if DEBUG +struct StateContainer: View { + @State private var state: T + private var content: (Binding) -> Content + + init(initialState: T, content: @escaping (Binding) -> Content) { + self._state = State(initialValue: initialState) + self.content = content + } + + var body: some View { + content($state) + } +} +#endif diff --git a/secant/Util/WithStateBinding.swift b/secant/Util/WithStateBinding.swift new file mode 100644 index 0000000..5db8c29 --- /dev/null +++ b/secant/Util/WithStateBinding.swift @@ -0,0 +1,59 @@ +import SwiftUI + +struct WithStateBinding: View { + @State var localState: T + @Binding private var externalBindng: T + private var content: (Binding) -> Content + + init(binding: Binding, content: @escaping (Binding) -> Content) { + _externalBindng = binding + _localState = State(initialValue: binding.wrappedValue) + self.content = content + } + + var body: some View { + content($localState) + .onChange(of: localState) { externalBindng = $0 } + } +} + +// MARK: - Previews + +struct WithStateBinding_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + StateContainer(initialState: (false, false, false)) { (binding: Binding<(Bool, Bool, Bool)>) in + List { + NavigationLink( + isActive: binding.0, + destination: { Text("Standard State Binding") }, + label: { Text("Standard State Binding") } + ) + + NavigationLink( + isActive: Binding( + get: { binding.1.wrappedValue }, + set: { binding.1.wrappedValue = $0 } + ), + destination: { Text("Custom Binding") }, + label: { Text("Custom Binding") } + ) + + WithStateBinding( + binding: Binding( + get: { binding.2.wrappedValue }, + set: { binding.2.wrappedValue = $0 } + ), + content: { + NavigationLink( + isActive:$0, + destination: { Text("Wrapped Custom Binding") }, + label: { Text("Wrapped Custom Binding") } + ) + } + ) + } + } + } + } +}