Files
koins/apps/ios/Koins/Views/AccountStack.swift
T

115 lines
3.8 KiB
Swift

import SwiftUI
/// A stack of account cards, Cake Wallet style: the front card is full size and
/// interactive, the rest peek from behind (scaled down and offset upwards).
/// Drag the front card sideways or down to send it to the back of the deck.
struct AccountStack: View {
var accounts: [MoneroAccount]
/// Converts an account's XMR amount to the chosen fiat currency, if a price
/// is available.
var fiat: (String) -> String?
var onQR: (MoneroAccount) -> Void
@State private var frontIndex = 0
@State private var dragOffset = CGSize.zero
private let maxDepth = 3
var body: some View {
ZStack(alignment: .top) {
ForEach(Array(accounts.enumerated()), id: \.element.id) { index, account in
let depth = depth(of: index)
accountCard(account, depth: depth)
.offset(x: depth == 0 ? dragOffset.width : 0,
y: depth == 0 ? dragOffset.height : 0)
.gesture(depth == 0 ? frontDrag : nil)
.allowsHitTesting(depth == 0)
.zIndex(Double(accounts.count - depth))
}
}
.frame(height: 190 + CGFloat(min(accounts.count - 1, maxDepth)) * 12)
.accessibilityHint("Drag the front card to cycle accounts")
}
// MARK: - Layout
/// How far behind the front an account sits (0 = front).
private func depth(of index: Int) -> Int {
(index - frontIndex + accounts.count) % accounts.count
}
private func accountCard(_ account: MoneroAccount, depth: Int) -> some View {
Card {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(account.title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
Spacer()
Text("\(depth + 1) of \(accounts.count)")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
VStack(alignment: .leading, spacing: 3) {
Text("\(account.balance) XMR")
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if let fiat = fiat(account.balance) {
Text(fiat)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.muted)
}
if account.unlocked != account.balance {
Text("\(account.unlocked) XMR unlocked")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
HStack(spacing: 8) {
Text(account.address)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(Theme.muted)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 6)
Button {
onQR(account)
} label: {
Image(systemName: "qrcode")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.accent)
.frame(width: 34, height: 34)
.background(Theme.surfaceAlt)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
}
}
// Cards behind the front one peek from the top, slightly smaller.
.scaleEffect(1 - 0.045 * CGFloat(min(depth, maxDepth)))
.offset(y: -CGFloat(min(depth, maxDepth)) * 12)
.opacity(depth > maxDepth ? 0 : 1)
}
// MARK: - Interaction
private var frontDrag: some Gesture {
DragGesture()
.onChanged { value in
dragOffset = value.translation
}
.onEnded { value in
let threshold: CGFloat = 90
let shouldAdvance = abs(value.translation.width) > threshold
|| value.translation.height > threshold
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
dragOffset = .zero
if shouldAdvance {
frontIndex = (frontIndex + 1) % accounts.count
}
}
}
}
}