Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion BDKSwiftExampleWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
AEAB03132ABDDBF4000C9528 /* AmountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */; };
AEAF83B62B7BD4D10019B23B /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = AEAF83B52B7BD4D10019B23B /* CodeScanner */; };
AEB130C92A44E4850087785B /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB130C82A44E4850087785B /* TransactionDetailView.swift */; };
AEB159D32D51A7E00006AE9E /* BalanceDisplayFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */; };
AEB159D52D51A8680006AE9E /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB159D42D51A8680006AE9E /* View+Extensions.swift */; };
AEB6C9D12B7E8529003AD704 /* TransactionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB6C9D02B7E8529003AD704 /* TransactionDetailViewModel.swift */; };
AEB735D32B2CC4B900F99DBB /* BitcoinUI in Frameworks */ = {isa = PBXBuildFile; productRef = AEB735D22B2CC4B900F99DBB /* BitcoinUI */; };
AEB905C32A7EEBF000CD0337 /* BackupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */; };
Expand Down Expand Up @@ -167,6 +169,8 @@
AEAB03102ABDDB86000C9528 /* FeeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeViewModel.swift; sourceTree = "<group>"; };
AEAB03122ABDDBF4000C9528 /* AmountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountViewModel.swift; sourceTree = "<group>"; };
AEB130C82A44E4850087785B /* TransactionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; };
AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceDisplayFormat.swift; sourceTree = "<group>"; };
AEB159D42D51A8680006AE9E /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
AEB6C9D02B7E8529003AD704 /* TransactionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailViewModel.swift; sourceTree = "<group>"; };
AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupInfo.swift; sourceTree = "<group>"; };
AEC2CF592ABFBA19008065E4 /* BuildTransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildTransactionViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -241,6 +245,8 @@
AE783A042AB4F51F005F0CBA /* String+Extensions.swift */,
AE287E762C0F6D200036A748 /* Array+Extensions.swift */,
AE7F67062A744CE200CED561 /* Double+Extensions.swift */,
AEB159D42D51A8680006AE9E /* View+Extensions.swift */,
AE8D001B2D19F1760029C4C9 /* UIScreen+Extensions.swift */,
AEE6C74D2ABCB48600442ADD /* BDK+Extensions */,
);
path = Extensions;
Expand Down Expand Up @@ -442,6 +448,7 @@
isa = PBXGroup;
children = (
AE7F67082A7451AA00CED561 /* Price.swift */,
AEB159D22D51A7E00006AE9E /* BalanceDisplayFormat.swift */,
AE2B8C1E2A96797300815B2F /* RecommendedFees.swift */,
AE7F670B2A7451D700CED561 /* CurrencyCode.swift */,
AEB905C22A7EEBF000CD0337 /* BackupInfo.swift */,
Expand Down Expand Up @@ -521,7 +528,6 @@
AE184EFB2BFE52C800374362 /* Amount+Extensions.swift */,
AE91CEEC2C0FDB70000AAD20 /* SentAndReceivedValues+Extensions.swift */,
AE91CEEE2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift */,
AE8D001B2D19F1760029C4C9 /* UIScreen+Extensions.swift */,
);
path = "BDK+Extensions";
sourceTree = "<group>";
Expand Down Expand Up @@ -661,13 +667,15 @@
AE7F670C2A7451D700CED561 /* CurrencyCode.swift in Sources */,
AE2ADD762B61EFEB00C2A823 /* HomeViewModel.swift in Sources */,
AE783A032AB4ECC2005F0CBA /* AddressView.swift in Sources */,
AEB159D52D51A8680006AE9E /* View+Extensions.swift in Sources */,
AE7F67052A7446B600CED561 /* PriceService.swift in Sources */,
AEAB03132ABDDBF4000C9528 /* AmountViewModel.swift in Sources */,
AE7953902A2D5B4400CCB277 /* BDKSwiftExampleWalletError.swift in Sources */,
AE91CEEF2C0FDBC7000AAD20 /* CanonicalTx+Extensions.swift in Sources */,
AE2381B52C60878E00F6B00C /* LocalOutputItemView.swift in Sources */,
AE3646262BEDB01200B04E25 /* FileManager+Extensions.swift in Sources */,
AEB6C9D12B7E8529003AD704 /* TransactionDetailViewModel.swift in Sources */,
AEB159D32D51A7E00006AE9E /* BalanceDisplayFormat.swift in Sources */,
AE18E9382A9528200019D2A4 /* Bundle+Extensions.swift in Sources */,
AE79538E2A2D59F000CCB277 /* Constants.swift in Sources */,
AE2F255D2BED0BFB002A9AC6 /* AppError.swift in Sources */,
Expand Down
33 changes: 33 additions & 0 deletions BDKSwiftExampleWallet/Extensions/View+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// View+Extensions.swift
// BDKSwiftExampleWallet
//
// Created by Matthew Ramsden on 2/3/25.
//

import Foundation
import SwiftUI

extension View {
func swipeGesture(perform action: @escaping (SwipeDirection) -> Void) -> some View {
gesture(
DragGesture(minimumDistance: 20)
.onEnded { value in
let horizontal = value.translation.width
let vertical = value.translation.height

if abs(horizontal) > abs(vertical) {
if horizontal > 0 {
action(.right)
} else {
action(.left)
}
}
}
)
}
}

enum SwipeDirection {
case left, right
}
29 changes: 29 additions & 0 deletions BDKSwiftExampleWallet/Model/BalanceDisplayFormat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// BalanceDisplayFormat.swift
// BDKSwiftExampleWallet
//
// Created by Matthew Ramsden on 2/3/25.
//

import Foundation

enum BalanceDisplayFormat: String, CaseIterable, Codable {
case sats = "sats"
case bitcoinSats = "bitcoinSats"
case bitcoin = "btc"
case fiat = "usd"

var displayText: String {
switch self {
case .sats, .bitcoinSats: return "sats"
case .bitcoin: return ""
case .fiat: return "USD"
}
}
}

extension BalanceDisplayFormat {
var index: Int {
BalanceDisplayFormat.allCases.firstIndex(of: self) ?? 0
}
}
4 changes: 3 additions & 1 deletion BDKSwiftExampleWallet/View Model/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import BitcoinDevKit
import Foundation
import Observation
import SwiftUI

@MainActor
@Observable
Expand All @@ -25,7 +26,8 @@ class WalletViewModel {
var price: Double = 0.00
var progress: Float = 0.0
var recentTransactions: [CanonicalTx] {
Array(transactions.prefix(5))
let maxTransactions = UIScreen.main.isPhoneSE ? 4 : 5
return Array(transactions.prefix(maxTransactions))
}
var satsPrice: Double {
let usdValue = Double(balanceTotal).valueInUSD(price: price)
Expand Down
152 changes: 105 additions & 47 deletions BDKSwiftExampleWallet/View/WalletView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import BitcoinUI
import SwiftUI

struct WalletView: View {
@AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat =
.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
@State private var showReceiveView = false
@State private var showSettingsView = false
@State private var showingFormatMenu = false

var body: some View {

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

VStack(spacing: 10) {
withAnimation {
HStack(spacing: 15) {
Image(systemName: "bitcoinsign")
.foregroundStyle(.secondary)
.font(.title)
.fontWeight(.thin)
Text(viewModel.balanceTotal.formattedSatoshis())
.contentTransition(.numericText())
.fontWeight(.semibold)
.fontDesign(.rounded)
Text("sats")
.foregroundStyle(.secondary)
.fontWeight(.thin)
}
.font(.largeTitle)
.lineLimit(1)
.minimumScaleFactor(0.5)
HStack(spacing: 15) {
currencySymbol
balanceText
unitText
}
.accessibilityLabel("Bitcoin Balance")
.accessibilityValue("\(viewModel.balanceTotal.formattedSatoshis()) sats")
HStack {
if viewModel.walletSyncState == .syncing {
Image(systemName: "chart.bar.fill")
.symbolEffect(.variableColor.cumulative)
.transition(.symbolEffect(.appear))
.font(.largeTitle)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
.accessibilityLabel("Bitcoin Balance")
.accessibilityValue(formattedBalance)
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
balanceFormat =
BalanceDisplayFormat.allCases[
(balanceFormat.index + 1) % BalanceDisplayFormat.allCases.count
]
}
}
.swipeGesture { direction in
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
switch direction {
case .left:
balanceFormat =
BalanceDisplayFormat.allCases[
(balanceFormat.index + 1) % BalanceDisplayFormat.allCases.count
]
case .right:
balanceFormat =
BalanceDisplayFormat.allCases[
(balanceFormat.index - 1 + BalanceDisplayFormat.allCases.count)
% BalanceDisplayFormat.allCases.count
]
}
Text(
viewModel.satsPrice > 0 || viewModel.walletSyncState == .synced
? viewModel.satsPrice.formatted(.currency(code: "USD")) : ""
)
.fontDesign(.rounded)
.foregroundStyle(
viewModel.walletSyncState == .synced ? .secondary : .tertiary
)
.opacity(
viewModel.walletSyncState == .syncing && viewModel.satsPrice == 0
? 0.7 : 1
)
.contentTransition(.numericText())
.animation(
.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5),
value: viewModel.satsPrice
)
}
.foregroundStyle(.secondary)
.font(.subheadline)
.animation(
.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5),
value: viewModel.walletSyncState
)
}
.padding(.vertical, 35.0)

Expand Down Expand Up @@ -300,6 +289,75 @@ struct WalletView: View {

}

extension WalletView {

@MainActor
var formattedBalance: String {
switch balanceFormat {
case .sats:
return viewModel.balanceTotal.formatted(.number)
case .bitcoinSats:
return viewModel.balanceTotal.formattedSatoshis()
case .bitcoin:
return String(format: "%.8f", Double(viewModel.balanceTotal) / 100_000_000)
case .fiat:
return String(format: "%.2f", viewModel.satsPrice)
}
}

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 ? 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
#Preview("WalletView - en") {
WalletView(
Expand Down