Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions BDKSwiftExampleWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -108,6 +109,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
774586B42DB7B2BC00A631E1 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = "<group>"; };
77F0FDC82DA9A93700B30E4F /* Connection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Extensions.swift"; sourceTree = "<group>"; };
A733D6CF2A81113000F333B4 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
A73F7A352A3B778E00B87FC6 /* Int+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -268,6 +270,7 @@
AE2381B92C61255100F6B00C /* Receive */,
AE2381B82C61254B00F6B00C /* Send */,
AE2381B72C61254200F6B00C /* Settings */,
774586B42DB7B2BC00A631E1 /* BalanceView.swift */,
);
path = View;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -921,6 +925,7 @@
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\"";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
128 changes: 128 additions & 0 deletions BDKSwiftExampleWallet/View/BalanceView.swift
Original file line number Diff line number Diff line change
@@ -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
97 changes: 5 additions & 92 deletions BDKSwiftExampleWallet/View/WalletView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Matthew Ramsden on 5/23/23.
//

import BitcoinDevKit
import BitcoinUI
import SwiftUI

Expand All @@ -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
Expand All @@ -30,30 +28,18 @@ 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[
(balanceFormat.index + 1) % BalanceDisplayFormat.allCases.count
]
}
}
.sensoryFeedback(.selection, trigger: balanceFormat)
.padding(.vertical, 35.0)

VStack {
HStack {
Expand Down Expand Up @@ -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
Expand Down