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.
This commit is contained in:
Daniel Haight 2021-11-15 16:43:47 +00:00
parent 1adf39b933
commit c6a98f06c2
4 changed files with 91 additions and 5 deletions

View File

@ -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 = "<group>"; };
66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = "<group>"; };
F9322DBF273B555C00C105B5 /* NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationLinks.swift; sourceTree = "<group>"; };
F93673D52742CB840099C6AF /* Previews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = "<group>"; };
F93874ED273C4DE200F0E875 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = "<group>"; };
F93874EF273C4DE200F0E875 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryStore.swift; sourceTree = "<group>"; };
@ -212,6 +215,7 @@
F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryView.swift; sourceTree = "<group>"; };
F96B41EA273B50520021B49A /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
F9C165B3274031F600592F76 /* Bindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bindings.swift; sourceTree = "<group>"; };
F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithStateBinding.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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 */,

View File

@ -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"))

View File

@ -0,0 +1,17 @@
import SwiftUI
#if DEBUG
struct StateContainer<T, Content: View>: View {
@State private var state: T
private var content: (Binding<T>) -> Content
init(initialState: T, content: @escaping (Binding<T>) -> Content) {
self._state = State(initialValue: initialState)
self.content = content
}
var body: some View {
content($state)
}
}
#endif

View File

@ -0,0 +1,59 @@
import SwiftUI
struct WithStateBinding<T: Equatable, Content: View>: View {
@State var localState: T
@Binding private var externalBindng: T
private var content: (Binding<T>) -> Content
init(binding: Binding<T>, content: @escaping (Binding<T>) -> 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") }
)
}
)
}
}
}
}
}