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 } } } } }