Skip to content

Commit 13eef9f

Browse files
authored
feat: balance format selection
1 parent 5af5ed4 commit 13eef9f

File tree

6 files changed

+179
-49
lines changed

6 files changed

+179
-49
lines changed

BDKSwiftExampleWallet.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
AEAB03132ABDDBF4000C9528 /* AmountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */; };
7575
AEAF83B62B7BD4D10019B23B /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = AEAF83B52B7BD4D10019B23B /* CodeScanner */; };
7676
AEB130C92A44E4850087785B /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB130C82A44E4850087785B /* TransactionDetailView.swift */; };
77+
AEB159D32D51A7E00006AE9E /* BalanceDisplayFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */; };
78+
AEB159D52D51A8680006AE9E /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB159D42D51A8680006AE9E /* View+Extensions.swift */; };
7779
AEB6C9D12B7E8529003AD704 /* TransactionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB6C9D02B7E8529003AD704 /* TransactionDetailViewModel.swift */; };
7880
AEB735D32B2CC4B900F99DBB /* BitcoinUI in Frameworks */ = {isa = PBXBuildFile; productRef = AEB735D22B2CC4B900F99DBB /* BitcoinUI */; };
7981
AEB905C32A7EEBF000CD0337 /* BackupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */; };
@@ -167,6 +169,8 @@
167169
AEAB03102ABDDB86000C9528 /* FeeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeViewModel.swift; sourceTree = "<group>"; };
168170
AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountViewModel.swift; sourceTree = "<group>"; };
169171
AEB130C82A44E4850087785B /* TransactionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; };
172+
AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceDisplayFormat.swift; sourceTree = "<group>"; };
173+
AEB159D42D51A8680006AE9E /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
170174
AEB6C9D02B7E8529003AD704 /* TransactionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailViewModel.swift; sourceTree = "<group>"; };
171175
AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupInfo.swift; sourceTree = "<group>"; };
172176
AEC2CF592ABFBA19008065E4 /* BuildTransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildTransactionViewModel.swift; sourceTree = "<group>"; };
@@ -241,6 +245,8 @@
241245
AE783A042AB4F51F005F0CBA /* String+Extensions.swift */,
242246
AE287E762C0F6D200036A748 /* Array+Extensions.swift */,
243247
AE7F67062A744CE200CED561 /* Double+Extensions.swift */,
248+
AEB159D42D51A8680006AE9E /* View+Extensions.swift */,
249+
AE8D001B2D19F1760029C4C9 /* UIScreen+Extensions.swift */,
244250
AEE6C74D2ABCB48600442ADD /* BDK+Extensions */,
245251
);
246252
path = Extensions;
@@ -442,6 +448,7 @@
442448
isa = PBXGroup;
443449
children = (
444450
AE7F67082A7451AA00CED561 /* Price.swift */,
451+
AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */,
445452
AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */,
446453
AE7F670B2A7451D700CED561 /* CurrencyCode.swift */,
447454
AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */,
@@ -521,7 +528,6 @@
521528
AE184EFB2BFE52C800374362 /* Amount+Extensions.swift */,
522529
AE91CEEC2C0FDB70000AAD20 /* SentAndReceivedValues+Extensions.swift */,
523530
AE91CEEE2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift */,
524-
AE8D001B2D19F1760029C4C9 /* UIScreen+Extensions.swift */,
525531
);
526532
path = "BDK+Extensions";
527533
sourceTree = "<group>";
@@ -661,13 +667,15 @@
661667
AE7F670C2A7451D700CED561 /* CurrencyCode.swift in Sources */,
662668
AE2ADD762B61EFEB00C2A823 /* HomeViewModel.swift in Sources */,
663669
AE783A032AB4ECC2005F0CBA /* AddressView.swift in Sources */,
670+
AEB159D52D51A8680006AE9E /* View+Extensions.swift in Sources */,
664671
AE7F67052A7446B600CED561 /* PriceService.swift in Sources */,
665672
AEAB03132ABDDBF4000C9528 /* AmountViewModel.swift in Sources */,
666673
AE7953902A2D5B4400CCB277 /* BDKSwiftExampleWalletError.swift in Sources */,
667674
AE91CEEF2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift in Sources */,
668675
AE2381B52C60878E00F6B00C /* LocalOutputItemView.swift in Sources */,
669676
AE3646262BEDB01200B04E25 /* FileManager+Extensions.swift in Sources */,
670677
AEB6C9D12B7E8529003AD704 /* TransactionDetailViewModel.swift in Sources */,
678+
AEB159D32D51A7E00006AE9E /* BalanceDisplayFormat.swift in Sources */,
671679
AE18E9382A9528200019D2A4 /* Bundle+Extensions.swift in Sources */,
672680
AE79538E2A2D59F000CCB277 /* Constants.swift in Sources */,
673681
AE2F255D2BED0BFB002A9AC6 /* AppError.swift in Sources */,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// View+Extensions.swift
3+
// BDKSwiftExampleWallet
4+
//
5+
// Created by Matthew Ramsden on 2/3/25.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
extension View {
12+
func swipeGesture(perform action: @escaping (SwipeDirection) -> Void) -> some View {
13+
gesture(
14+
DragGesture(minimumDistance: 20)
15+
.onEnded { value in
16+
let horizontal = value.translation.width
17+
let vertical = value.translation.height
18+
19+
if abs(horizontal) > abs(vertical) {
20+
if horizontal > 0 {
21+
action(.right)
22+
} else {
23+
action(.left)
24+
}
25+
}
26+
}
27+
)
28+
}
29+
}
30+
31+
enum SwipeDirection {
32+
case left, right
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// BalanceDisplayFormat.swift
3+
// BDKSwiftExampleWallet
4+
//
5+
// Created by Matthew Ramsden on 2/3/25.
6+
//
7+
8+
import Foundation
9+
10+
enum BalanceDisplayFormat: String, CaseIterable, Codable {
11+
case sats = "sats"
12+
case bitcoinSats = "bitcoinSats"
13+
case bitcoin = "btc"
14+
case fiat = "usd"
15+
16+
var displayText: String {
17+
switch self {
18+
case .sats, .bitcoinSats: return "sats"
19+
case .bitcoin: return ""
20+
case .fiat: return "USD"
21+
}
22+
}
23+
}
24+
25+
extension BalanceDisplayFormat {
26+
var index: Int {
27+
BalanceDisplayFormat.allCases.firstIndex(of: self) ?? 0
28+
}
29+
}

BDKSwiftExampleWallet/View Model/WalletViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import BitcoinDevKit
99
import Foundation
1010
import Observation
11+
import SwiftUI
1112

1213
@MainActor
1314
@Observable
@@ -25,7 +26,8 @@ class WalletViewModel {
2526
var price: Double = 0.00
2627
var progress: Float = 0.0
2728
var recentTransactions: [CanonicalTx] {
28-
Array(transactions.prefix(5))
29+
let maxTransactions = UIScreen.main.isPhoneSE ? 4 : 5
30+
return Array(transactions.prefix(maxTransactions))
2931
}
3032
var satsPrice: Double {
3133
let usdValue = Double(balanceTotal).valueInUSD(price: price)

BDKSwiftExampleWallet/View/WalletView.swift

Lines changed: 105 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ import BitcoinUI
1010
import SwiftUI
1111

1212
struct WalletView: View {
13+
@AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat =
14+
.bitcoinSats
1315
@Bindable var viewModel: WalletViewModel
1416
@Binding var sendNavigationPath: NavigationPath
17+
@State private var balanceTextPulsingOpacity: Double = 0.7
1518
@State private var isFirstAppear = true
1619
@State private var newTransactionSent = false
1720
@State private var showAllTransactions = false
1821
@State private var showReceiveView = false
1922
@State private var showSettingsView = false
23+
@State private var showingFormatMenu = false
2024

2125
var body: some View {
2226

@@ -27,56 +31,41 @@ struct WalletView: View {
2731
VStack(spacing: 20) {
2832

2933
VStack(spacing: 10) {
30-
withAnimation {
31-
HStack(spacing: 15) {
32-
Image(systemName: "bitcoinsign")
33-
.foregroundStyle(.secondary)
34-
.font(.title)
35-
.fontWeight(.thin)
36-
Text(viewModel.balanceTotal.formattedSatoshis())
37-
.contentTransition(.numericText())
38-
.fontWeight(.semibold)
39-
.fontDesign(.rounded)
40-
Text("sats")
41-
.foregroundStyle(.secondary)
42-
.fontWeight(.thin)
43-
}
44-
.font(.largeTitle)
45-
.lineLimit(1)
46-
.minimumScaleFactor(0.5)
34+
HStack(spacing: 15) {
35+
currencySymbol
36+
balanceText
37+
unitText
4738
}
48-
.accessibilityLabel("Bitcoin Balance")
49-
.accessibilityValue("\(viewModel.balanceTotal.formattedSatoshis()) sats")
50-
HStack {
51-
if viewModel.walletSyncState == .syncing {
52-
Image(systemName: "chart.bar.fill")
53-
.symbolEffect(.variableColor.cumulative)
54-
.transition(.symbolEffect(.appear))
39+
.font(.largeTitle)
40+
.lineLimit(1)
41+
.minimumScaleFactor(0.5)
42+
}
43+
.accessibilityLabel("Bitcoin Balance")
44+
.accessibilityValue(formattedBalance)
45+
.onTapGesture {
46+
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
47+
balanceFormat =
48+
BalanceDisplayFormat.allCases[
49+
(balanceFormat.index + 1) % BalanceDisplayFormat.allCases.count
50+
]
51+
}
52+
}
53+
.swipeGesture { direction in
54+
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
55+
switch direction {
56+
case .left:
57+
balanceFormat =
58+
BalanceDisplayFormat.allCases[
59+
(balanceFormat.index + 1) % BalanceDisplayFormat.allCases.count
60+
]
61+
case .right:
62+
balanceFormat =
63+
BalanceDisplayFormat.allCases[
64+
(balanceFormat.index - 1 + BalanceDisplayFormat.allCases.count)
65+
% BalanceDisplayFormat.allCases.count
66+
]
5567
}
56-
Text(
57-
viewModel.satsPrice > 0 || viewModel.walletSyncState == .synced
58-
? viewModel.satsPrice.formatted(.currency(code: "USD")) : ""
59-
)
60-
.fontDesign(.rounded)
61-
.foregroundStyle(
62-
viewModel.walletSyncState == .synced ? .secondary : .tertiary
63-
)
64-
.opacity(
65-
viewModel.walletSyncState == .syncing && viewModel.satsPrice == 0
66-
? 0.7 : 1
67-
)
68-
.contentTransition(.numericText())
69-
.animation(
70-
.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5),
71-
value: viewModel.satsPrice
72-
)
7368
}
74-
.foregroundStyle(.secondary)
75-
.font(.subheadline)
76-
.animation(
77-
.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5),
78-
value: viewModel.walletSyncState
79-
)
8069
}
8170
.padding(.vertical, 35.0)
8271

@@ -300,6 +289,75 @@ struct WalletView: View {
300289

301290
}
302291

292+
extension WalletView {
293+
294+
@MainActor
295+
var formattedBalance: String {
296+
switch balanceFormat {
297+
case .sats:
298+
return viewModel.balanceTotal.formatted(.number)
299+
case .bitcoinSats:
300+
return viewModel.balanceTotal.formattedSatoshis()
301+
case .bitcoin:
302+
return String(format: "%.8f", Double(viewModel.balanceTotal) / 100_000_000)
303+
case .fiat:
304+
return String(format: "%.2f", viewModel.satsPrice)
305+
}
306+
}
307+
308+
private var currencySymbol: some View {
309+
Image(systemName: balanceFormat == .fiat ? "dollarsign" : "bitcoinsign")
310+
.foregroundStyle(.secondary)
311+
.font(.title)
312+
.fontWeight(.thin)
313+
.transition(
314+
.asymmetric(
315+
insertion: .move(edge: .leading).combined(with: .opacity),
316+
removal: .move(edge: .trailing).combined(with: .opacity)
317+
)
318+
)
319+
.opacity(balanceFormat == .sats ? 0 : 1)
320+
.id("symbol-\(balanceFormat)")
321+
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: balanceFormat)
322+
}
323+
324+
@MainActor
325+
var balanceText: some View {
326+
Text(balanceFormat == .fiat && viewModel.satsPrice == 0 ? "00.00" : formattedBalance)
327+
.contentTransition(.numericText(countsDown: true))
328+
.fontWeight(.semibold)
329+
.fontDesign(.rounded)
330+
.foregroundStyle(
331+
balanceFormat == .fiat && viewModel.satsPrice == 0 ? .secondary : .primary
332+
)
333+
.opacity(
334+
balanceFormat == .fiat && viewModel.satsPrice == 0 ? balanceTextPulsingOpacity : 1
335+
)
336+
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: balanceFormat)
337+
.animation(.easeInOut, value: viewModel.satsPrice)
338+
.onAppear {
339+
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
340+
balanceTextPulsingOpacity = 0.3
341+
}
342+
}
343+
}
344+
345+
private var unitText: some View {
346+
Text(balanceFormat.displayText)
347+
.foregroundStyle(.secondary)
348+
.fontWeight(.thin)
349+
.transition(
350+
.asymmetric(
351+
insertion: .move(edge: .trailing).combined(with: .opacity),
352+
removal: .move(edge: .leading).combined(with: .opacity)
353+
)
354+
)
355+
.id("format-\(balanceFormat)")
356+
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: balanceFormat)
357+
}
358+
359+
}
360+
303361
#if DEBUG
304362
#Preview("WalletView - en") {
305363
WalletView(

0 commit comments

Comments
 (0)