From 2a43d8d69946942dcd1de331f539a7ba2951ce88 Mon Sep 17 00:00:00 2001 From: Rubens Date: Tue, 22 Apr 2025 11:27:51 -0300 Subject: [PATCH 1/4] refactor: extract BalanceView to simplify WalletView and modularize balance display --- .../project.pbxproj | 16 ++- BDKSwiftExampleWallet/View/BalanceView.swift | 128 ++++++++++++++++++ BDKSwiftExampleWallet/View/WalletView.swift | 99 +------------- 3 files changed, 146 insertions(+), 97 deletions(-) create mode 100644 BDKSwiftExampleWallet/View/BalanceView.swift diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index b8aa9bb1..e93604fc 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 */, @@ -886,10 +890,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; - DEVELOPMENT_TEAM = WUQYBPQJWN; + DEVELOPMENT_TEAM = 9BV8XV2YX6; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; @@ -907,8 +912,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet.test; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -921,10 +927,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; - DEVELOPMENT_TEAM = WUQYBPQJWN; + DEVELOPMENT_TEAM = 9BV8XV2YX6; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; @@ -942,8 +949,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet.test; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/BDKSwiftExampleWallet/View/BalanceView.swift b/BDKSwiftExampleWallet/View/BalanceView.swift new file mode 100644 index 00000000..f9415dad --- /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..565eafed 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 @@ -29,22 +27,12 @@ struct WalletView: View { .ignoresSafeArea() 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 From 38a55450f4594a122c24bb6ed984d0667f24f8b5 Mon Sep 17 00:00:00 2001 From: Rubens Date: Tue, 22 Apr 2025 17:04:03 -0300 Subject: [PATCH 2/4] chore: restore .pbxproj changes --- BDKSwiftExampleWallet.xcodeproj/project.pbxproj | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index e93604fc..1a285acc 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -890,11 +890,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; - DEVELOPMENT_TEAM = 9BV8XV2YX6; + DEVELOPMENT_TEAM = WUQYBPQJWN; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; @@ -912,9 +911,8 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet.test; + PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -931,7 +929,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\""; - DEVELOPMENT_TEAM = 9BV8XV2YX6; + DEVELOPMENT_TEAM = WUQYBPQJWN; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BDKSwiftExampleWallet/Info.plist; @@ -949,9 +947,8 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet.test; + PRODUCT_BUNDLE_IDENTIFIER = com.bitcoindevkit.bdkswiftexamplewallet; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; From 233e5f63079932d97399c3b17c221ec6116e6adf Mon Sep 17 00:00:00 2001 From: Rubens Date: Tue, 22 Apr 2025 17:07:26 -0300 Subject: [PATCH 3/4] chore: swift-format changes --- .../View Model/Receive/ReceiveViewModel.swift | 2 +- BDKSwiftExampleWallet/View/BalanceView.swift | 32 +++++++++---------- BDKSwiftExampleWallet/View/WalletView.swift | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) 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 index f9415dad..6b154b7b 100644 --- a/BDKSwiftExampleWallet/View/BalanceView.swift +++ b/BDKSwiftExampleWallet/View/BalanceView.swift @@ -8,9 +8,9 @@ import SwiftUI struct BalanceView: View { - + @State private var balanceTextPulsingOpacity: Double = 0.7 - + private var format: BalanceDisplayFormat private let balance: UInt64 private var fiatPrice: Double @@ -18,7 +18,7 @@ struct BalanceView: View { let usdValue = Double(balance).valueInUSD(price: fiatPrice) return usdValue } - + private var currencySymbol: some View { Image(systemName: format == .fiat ? "dollarsign" : "bitcoinsign") .foregroundStyle(.secondary) @@ -34,7 +34,7 @@ struct BalanceView: View { .id("symbol-\(format)") .animation(.spring(response: 0.3, dampingFraction: 0.7), value: format) } - + @MainActor private var formattedBalance: String { switch format { @@ -50,7 +50,7 @@ struct BalanceView: View { return satsPrice.formatted(.number.precision(.fractionLength(2))) } } - + @MainActor var balanceText: some View { Text(format == .fiat && satsPrice == 0 ? "00.00" : formattedBalance) @@ -71,7 +71,7 @@ struct BalanceView: View { } } } - + private var unitText: some View { Text(format.displayText) .foregroundStyle(.secondary) @@ -85,17 +85,17 @@ struct BalanceView: View { .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) { @@ -118,11 +118,11 @@ struct BalanceView: View { } #if DEBUG -#Preview { - BalanceView( - format: .bip21q, - balance: 5000, - fiatPrice: 89000 - ) -} + #Preview { + BalanceView( + format: .bip21q, + balance: 5000, + fiatPrice: 89000 + ) + } #endif diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 565eafed..090cfe99 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -27,7 +27,7 @@ struct WalletView: View { .ignoresSafeArea() VStack(spacing: 20) { - + BalanceView( format: balanceFormat, balance: viewModel.balanceTotal, From 40df891096123c1c95bfe382e6ca43b6c652f504 Mon Sep 17 00:00:00 2001 From: Rubens Date: Tue, 22 Apr 2025 18:13:13 -0300 Subject: [PATCH 4/4] chore: removed .pbxproj change --- BDKSwiftExampleWallet.xcodeproj/project.pbxproj | 1 - 1 file changed, 1 deletion(-) diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 1a285acc..15bda4f0 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -925,7 +925,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = BDKSwiftExampleWallet/BDKSwiftExampleWallet.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"BDKSwiftExampleWallet/Preview Content\"";