Add `History` TCA feature

This adds a "TCA" feature of a (very) basic transaction history and detail.

It demonstrates a purely state driven navigation stack.
Specifically, a `route: Route?` value is tracked in the state.
This value is driven by the selection of a transaction in the list,
setting it to to `.selectedTransaction(transaction)`,
which then pushes the detail view for that `transaction` onto the `NavigationView`.
Popping the detail view sets the property to `nil`.

Take a look at the previews in `TransactionHistoryView` try them "live" as well.

**N.B** The models are _not_ correct in any way, though are meant to be somewhat representative
and give something to display.
This commit is contained in:
Daniel Haight 2021-11-15 23:32:54 +00:00
parent 94a0684380
commit 0ce7d14c81
5 changed files with 211 additions and 0 deletions

View File

@ -89,6 +89,10 @@
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 */; };
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 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -196,6 +200,10 @@
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
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>"; };
F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryStore.swift; sourceTree = "<group>"; };
F96B41E5273B501F0021B49A /* TransactionDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -525,6 +533,7 @@
children = (
0DACFA7E27208CE00039EEA5 /* Clamped.swift */,
0DACFA8027208D940039EEA5 /* UInt+SuperscriptText.swift */,
F96B41EA273B50520021B49A /* Strings.swift */,
);
path = Util;
sourceTree = "<group>";
@ -581,6 +590,7 @@
6654C73B2715A3F000901167 /* Features */ = {
isa = PBXGroup;
children = (
F96B41E2273B501F0021B49A /* TransactionHistory */,
6654C73C2715A3FA00901167 /* Onboarding */,
);
path = Features;
@ -637,6 +647,24 @@
path = CircularFrame;
sourceTree = "<group>";
};
F96B41E2273B501F0021B49A /* TransactionHistory */ = {
isa = PBXGroup;
children = (
F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */,
F96B41E4273B501F0021B49A /* Views */,
);
path = TransactionHistory;
sourceTree = "<group>";
};
F96B41E4273B501F0021B49A /* Views */ = {
isa = PBXGroup;
children = (
F96B41E5273B501F0021B49A /* TransactionDetailView.swift */,
F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -835,9 +863,11 @@
buildActionMask = 2147483647;
files = (
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
0D32281F26C5867D00262533 /* ScanQrScreenViewModel.swift in Sources */,
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
0D32282E26C5870B00262533 /* SendScreenViewModel.swift in Sources */,
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
0D32282D26C5870B00262533 /* SendScreen.swift in Sources */,
663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */,
0D1922ED26BDE0C600052649 /* AppRouter.swift in Sources */,
@ -866,6 +896,7 @@
0D170A7226BC802800EB6A46 /* Router.swift in Sources */,
0D354A0926D5A9D000315F45 /* Services.swift in Sources */,
660558F7270C862F009D6954 /* Fonts+Generated.swift in Sources */,
F96B41E7273B501F0021B49A /* TransactionHistoryStore.swift in Sources */,
0DA13C9726C186FF00E3B610 /* RestoreWalletScreen.swift in Sources */,
0DACFA8127208D940039EEA5 /* UInt+SuperscriptText.swift in Sources */,
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */,
@ -886,6 +917,7 @@
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */,
0DA13C8F26C15D1D00E3B610 /* WelcomeScreen.swift in Sources */,
0D32282826C586E000262533 /* RequestZcashScreen.swift in Sources */,
F96B41EB273B50520021B49A /* Strings.swift in Sources */,
0D32283226C5877A00262533 /* BalanceScreen.swift in Sources */,
0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */,
0DA13CA226C1955600E3B610 /* HomeScreenViewModel.swift in Sources */,

View File

@ -0,0 +1,58 @@
import ComposableArchitecture
import SwiftUI
struct Transaction: Identifiable, Equatable, Hashable {
var id: Int
var amount: UInt
var memo: String
var toAddress: String
var fromAddress: String
}
struct TransactionHistoryState: Equatable {
enum Route: Equatable {
case showTransaction(Transaction)
}
var transactions: IdentifiedArrayOf<Transaction>
var route: Route?
}
enum TransactionHistoryAction: Equatable {
case setRoute(TransactionHistoryState.Route?)
}
// MARK: - TransactionHistoryReducer
typealias TransactionHistoryReducer = Reducer<TransactionHistoryState, TransactionHistoryAction, Void>
extension TransactionHistoryReducer {
static let `default` = TransactionHistoryReducer { state, action, _ in
switch action {
case let .setRoute(route):
state.route = route
return .none
}
}
}
// MARK: - TransactionHistoryStore
typealias TransactionHistoryStore = Store<TransactionHistoryState, TransactionHistoryAction>
// MARK: - TransactionHistoryViewStore
typealias TransactionHistoryViewStore = ViewStore<TransactionHistoryState, TransactionHistoryAction>
extension TransactionHistoryViewStore {
private typealias Route = TransactionHistoryState.Route
func bindingForSelectingTransaction(_ transaction: Transaction) -> Binding<Bool> {
self.binding(
get: { $0.route.map(/TransactionHistoryState.Route.showTransaction) == transaction },
send: { isActive in
TransactionHistoryAction.setRoute( isActive ? TransactionHistoryState.Route.showTransaction(transaction) : nil)
}
)
}
}

View File

@ -0,0 +1,32 @@
import SwiftUI
struct TransactionDetailView: View {
var transaction: Transaction
var body: some View {
Text(String(dumping: transaction))
.padding()
.navigationTitle("Transaction: \(transaction.id)")
}
}
struct TransactionDetail_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TransactionDetailView(transaction: .demo)
}
}
}
#if DEBUG
extension Transaction {
static var demo: Self {
.init(
id: 2,
amount: 123,
memo: "defaultMemo",
toAddress: "ToAddress",
fromAddress: "FromAddress"
)
}
}
#endif

View File

@ -0,0 +1,78 @@
import SwiftUI
import ComposableArchitecture
struct TransactionHistoryView: View {
let store: Store<TransactionHistoryState, TransactionHistoryAction>
var body: some 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)") }
)
}
}
.navigationTitle(Text("Transactions"))
}
}
}
struct TransactionView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TransactionHistoryView(store: .demo)
.navigationBarTitleDisplayMode(.inline)
}
NavigationView {
TransactionHistoryView(store: .demoWithSelectedTransaction)
.navigationBarTitleDisplayMode(.inline)
}
}
}
#if DEBUG
extension TransactionHistoryStore {
static var demo: Store<TransactionHistoryState, TransactionHistoryAction> {
return Store(
initialState: TransactionHistoryState(
transactions: .demo,
route: nil
),
reducer: .default,
environment: ()
)
}
static var demoWithSelectedTransaction: Store<TransactionHistoryState, TransactionHistoryAction> {
let transactions = IdentifiedArrayOf<Transaction>.demo
return Store(
initialState: TransactionHistoryState(
transactions: transactions,
route: .showTransaction(transactions[3])
),
reducer: .default.debug(),
environment: ()
)
}
}
extension IdentifiedArrayOf where Element == Transaction {
static var demo: IdentifiedArrayOf<Transaction> {
return .init(
uniqueElements: (0..<10).map {
Transaction(
id: $0,
amount: 25,
memo: "defaultMemo",
toAddress: "ToAddress",
fromAddress: "FromAddress"
)
}
)
}
}
#endif

11
secant/Util/Strings.swift Normal file
View File

@ -0,0 +1,11 @@
import Foundation
#if DEBUG
extension String {
init<T>(dumping value: T) {
var output = String()
dump(value, to: &output)
self.init(stringLiteral: output)
}
}
#endif