diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index b8aa9bb1..15bda4f0 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 774586B52DB7B2BC00A631E1 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774586B42DB7B2BC00A631E1 /* BalanceView.swift */; }; 77F0FDC92DA9A93D00B30E4F /* Connection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F0FDC82DA9A93700B30E4F /* Connection+Extensions.swift */; }; A733D6D02A81113000F333B4 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A733D6CF2A81113000F333B4 /* Localizable.xcstrings */; }; A73F7A362A3B778E00B87FC6 /* Int+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73F7A352A3B778E00B87FC6 /* Int+Extensions.swift */; }; @@ -108,6 +109,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 774586B42DB7B2BC00A631E1 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = ""; }; 77F0FDC82DA9A93700B30E4F /* Connection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Extensions.swift"; sourceTree = ""; }; A733D6CF2A81113000F333B4 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A73F7A352A3B778E00B87FC6 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = ""; }; @@ -268,6 +270,7 @@ AE2381B92C61255100F6B00C /* Receive */, AE2381B82C61254B00F6B00C /* Send */, AE2381B72C61254200F6B00C /* Settings */, + 774586B42DB7B2BC00A631E1 /* BalanceView.swift */, ); path = View; sourceTree = ""; @@ -668,6 +671,7 @@ AEE6C74C2ABCB3E200442ADD /* Transaction+Extensions.swift in Sources */, AE0C30F72A804A2D008F1EAE /* TransactionListView.swift in Sources */, AE29ED152BBE36C500EB9C4F /* TransactionListViewModel.swift in Sources */, + 774586B52DB7B2BC00A631E1 /* BalanceView.swift in Sources */, AEB905C32A7EEBF000CD0337 /* BackupInfo.swift in Sources */, AE783A072AB4F7C7005F0CBA /* FeeView.swift in Sources */, AE2B8C1D2A9678C900815B2F /* FeeService.swift in Sources */, diff --git a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift index fd3ee8cc..7bc403e4 100644 --- a/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Receive/ReceiveViewModel.swift @@ -110,7 +110,7 @@ extension ReceiveViewModel { func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { if let message = messages.first, let record = message.records.first, - let _ = String(data: record.payload, encoding: .utf8) + String(data: record.payload, encoding: .utf8) != nil { // Handle response } diff --git a/BDKSwiftExampleWallet/View/BalanceView.swift b/BDKSwiftExampleWallet/View/BalanceView.swift new file mode 100644 index 00000000..6b154b7b --- /dev/null +++ b/BDKSwiftExampleWallet/View/BalanceView.swift @@ -0,0 +1,128 @@ +// +// BalanceView.swift +// BDKSwiftExampleWallet +// +// Created by Rubens Machion on 22/04/25. +// + +import SwiftUI + +struct BalanceView: View { + + @State private var balanceTextPulsingOpacity: Double = 0.7 + + private var format: BalanceDisplayFormat + private let balance: UInt64 + private var fiatPrice: Double + private var satsPrice: Double { + let usdValue = Double(balance).valueInUSD(price: fiatPrice) + return usdValue + } + + private var currencySymbol: some View { + Image(systemName: format == .fiat ? "dollarsign" : "bitcoinsign") + .foregroundStyle(.secondary) + .font(.title) + .fontWeight(.thin) + .transition( + .asymmetric( + insertion: .move(edge: .leading).combined(with: .opacity), + removal: .move(edge: .trailing).combined(with: .opacity) + ) + ) + .opacity(format == .sats || format == .bip21q ? 0 : 1) + .id("symbol-\(format)") + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: format) + } + + @MainActor + private var formattedBalance: String { + switch format { + case .sats: + return balance.formatted(.number) + case .bitcoin: + return String(format: "%.8f", Double(balance) / 100_000_000) + case .bitcoinSats: + return balance.formattedSatoshis() + case .bip21q: + return balance.formatted(.number) + case .fiat: + return satsPrice.formatted(.number.precision(.fractionLength(2))) + } + } + + @MainActor + var balanceText: some View { + Text(format == .fiat && satsPrice == 0 ? "00.00" : formattedBalance) + .contentTransition(.numericText(countsDown: true)) + .fontWeight(.semibold) + .fontDesign(.rounded) + .foregroundStyle( + format == .fiat && satsPrice == 0 ? .secondary : .primary + ) + .opacity( + format == .fiat && satsPrice == 0 ? balanceTextPulsingOpacity : 1 + ) + .animation(.spring(response: 0.4, dampingFraction: 0.8), value: format) + .animation(.easeInOut, value: satsPrice) + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + balanceTextPulsingOpacity = 0.3 + } + } + } + + private var unitText: some View { + Text(format.displayText) + .foregroundStyle(.secondary) + .fontWeight(.thin) + .transition( + .asymmetric( + insertion: .move(edge: .trailing).combined(with: .opacity), + removal: .move(edge: .leading).combined(with: .opacity) + ) + ) + .id("format-\(format)") + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: format) + } + + init(format: BalanceDisplayFormat, balance: UInt64, fiatPrice: Double) { + self.format = format + self.balance = balance + self.fiatPrice = fiatPrice + } + + var body: some View { + buildBalance() + } + + @ViewBuilder + private func buildBalance() -> some View { + VStack(spacing: 10) { + HStack(spacing: 15) { + if format != .sats && format != .bip21q { + currencySymbol + } + balanceText + unitText + } + .font(.largeTitle) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + .accessibilityLabel("Bitcoin Balance") + .accessibilityValue(formattedBalance) + .sensoryFeedback(.selection, trigger: format) + .padding(.vertical, 35.0) + } +} + +#if DEBUG + #Preview { + BalanceView( + format: .bip21q, + balance: 5000, + fiatPrice: 89000 + ) + } +#endif diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 0b6f5b9e..090cfe99 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -5,7 +5,6 @@ // Created by Matthew Ramsden on 5/23/23. // -import BitcoinDevKit import BitcoinUI import SwiftUI @@ -14,7 +13,6 @@ struct WalletView: View { .bitcoinSats @Bindable var viewModel: WalletViewModel @Binding var sendNavigationPath: NavigationPath - @State private var balanceTextPulsingOpacity: Double = 0.7 @State private var isFirstAppear = true @State private var newTransactionSent = false @State private var showAllTransactions = false @@ -30,21 +28,11 @@ struct WalletView: View { VStack(spacing: 20) { - VStack(spacing: 10) { - HStack(spacing: 15) { - if balanceFormat != .sats && balanceFormat != .bip21q { - currencySymbol - } - balanceText - unitText - } - .font(.largeTitle) - .lineLimit(1) - .minimumScaleFactor(0.5) - } - .accessibilityLabel("Bitcoin Balance") - .accessibilityValue(formattedBalance) - .onTapGesture { + BalanceView( + format: balanceFormat, + balance: viewModel.balanceTotal, + fiatPrice: viewModel.price + ).onTapGesture { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { balanceFormat = BalanceDisplayFormat.allCases[ @@ -52,8 +40,6 @@ struct WalletView: View { ] } } - .sensoryFeedback(.selection, trigger: balanceFormat) - .padding(.vertical, 35.0) VStack { HStack { @@ -270,80 +256,7 @@ struct WalletView: View { } } } - } - -} - -extension WalletView { - - @MainActor - var formattedBalance: String { - switch balanceFormat { - case .sats: - return viewModel.balanceTotal.formatted(.number) - case .bitcoin: - return String(format: "%.8f", Double(viewModel.balanceTotal) / 100_000_000) - case .bitcoinSats: - return viewModel.balanceTotal.formattedSatoshis() - case .bip21q: - return viewModel.balanceTotal.formatted(.number) - case .fiat: - return viewModel.satsPrice.formatted(.number.precision(.fractionLength(2))) - } - } - - private var currencySymbol: some View { - Image(systemName: balanceFormat == .fiat ? "dollarsign" : "bitcoinsign") - .foregroundStyle(.secondary) - .font(.title) - .fontWeight(.thin) - .transition( - .asymmetric( - insertion: .move(edge: .leading).combined(with: .opacity), - removal: .move(edge: .trailing).combined(with: .opacity) - ) - ) - .opacity(balanceFormat == .sats || balanceFormat == .bip21q ? 0 : 1) - .id("symbol-\(balanceFormat)") - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: balanceFormat) - } - - @MainActor - var balanceText: some View { - Text(balanceFormat == .fiat && viewModel.satsPrice == 0 ? "00.00" : formattedBalance) - .contentTransition(.numericText(countsDown: true)) - .fontWeight(.semibold) - .fontDesign(.rounded) - .foregroundStyle( - balanceFormat == .fiat && viewModel.satsPrice == 0 ? .secondary : .primary - ) - .opacity( - balanceFormat == .fiat && viewModel.satsPrice == 0 ? balanceTextPulsingOpacity : 1 - ) - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: balanceFormat) - .animation(.easeInOut, value: viewModel.satsPrice) - .onAppear { - withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { - balanceTextPulsingOpacity = 0.3 - } - } - } - - private var unitText: some View { - Text(balanceFormat.displayText) - .foregroundStyle(.secondary) - .fontWeight(.thin) - .transition( - .asymmetric( - insertion: .move(edge: .trailing).combined(with: .opacity), - removal: .move(edge: .leading).combined(with: .opacity) - ) - ) - .id("format-\(balanceFormat)") - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: balanceFormat) - } - } #if DEBUG