taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 7fadf6fb30ab53d59141434bbd06f62070a050a7
parent af55ce6ca58dac792823a6d2161b73cdc53fd485
Author: Marc Stibane <marc@taler.net>
Date:   Tue,  7 Jul 2026 20:19:21 +0200

Phase animation for actions

Diffstat:
MTalerWallet1/Views/Actions/ActionsSheet.swift | 19+++++++++++--------
DTalerWallet1/Views/Actions/DepositWithdrawV.swift | 33---------------------------------
ATalerWallet1/Views/Actions/PhaseView.swift | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DTalerWallet1/Views/Actions/SendRequestV.swift | 33---------------------------------
MTalerWallet1/Views/Actions/TwoRowButtons.swift | 43+++++++++++++++++++++++++++++++------------
MTalerWallet1/Views/HelperViews/IconBadge.swift | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++--
6 files changed, 176 insertions(+), 88 deletions(-)

diff --git a/TalerWallet1/Views/Actions/ActionsSheet.swift b/TalerWallet1/Views/Actions/ActionsSheet.swift @@ -34,7 +34,9 @@ struct ScannedURLs: View { Text(scannedURL.command.localizedCommand) let primaryAccent = WalletColors().primaryAccent ButtonIconBadge(type: scannedURL.command.transactionType, - foreColor: primaryAccent, done: false) + phase: 0, + foreColor: primaryAccent, + done: false) .talerFont(.title2) Spacer() @@ -134,18 +136,19 @@ struct ActionsSheet: View { let sendDisabled = noP2P let recvDisabled = noBalances || noP2P let depoDisabled = !mayDeposit - /// [􁉇 Send ] [􁉅 Request] - SendRequestV(stack: stack.push(), sendDisabled: sendDisabled, recvDisabled: recvDisabled) - .accessibility(sortPriority: 1) // read this after maps - /// [􁾩Deposit] [􁾭 Withdraw] - DepositWithdrawV(stack: stack.push(), sendDisabled: depoDisabled, recvDisabled: noBalances) - .accessibility(sortPriority: 0) // read this last + TimelineView(.periodic(from: .now, by: 60/80)) { context in + PhaseView(stack: stack.push(), date: context.date, + sendDisabled: sendDisabled, recvDisabled: recvDisabled, + depoDisabled: depoDisabled, wthdDisabled: noBalances) + } HStack { if #available(iOS 26.0, *) { if !pasteAutomatically { let isEnabled = UIPasteboard.general.hasURLs PasteButton(accessibilityLabelStr: "Paste") { - TalerWallet1App().inspectPasteboard(playSound: true) + Task { + TalerWallet1App().inspectPasteboard(playSound: true) + } }.buttonStyle(TalerButtonStyle(type: .bordered, dimmed: false, narrow: true, diff --git a/TalerWallet1/Views/Actions/DepositWithdrawV.swift b/TalerWallet1/Views/Actions/DepositWithdrawV.swift @@ -1,33 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI - -/// [􁾩Deposit] [􁾭 Withdraw] -struct DepositWithdrawV: View { - let stack: CallStack - let sendDisabled: Bool // can't send/deposit if wallet has no coins at all - let recvDisabled: Bool // can't receive/withdraw if wallet has no payments services - - var body: some View { - let depositTitle = String(localized: "DepositButton_Short", defaultValue: "Deposit", - comment: "Abbreviation of button `Deposit (currency)´") - let withdrawTitle = String(localized: "WithdrawButton_Short", defaultValue: "Withdraw", - comment: "Abbreviation of button `Withdraw (currency)´") - TwoRowButtons(stack: stack.push(), - sendTitle: depositTitle, - sendType: .deposit, - sendA11y: depositTitle, - recvTitle: withdrawTitle, - recvType: .withdrawal, - recvA11y: withdrawTitle, - sendDisabled: sendDisabled, - sendAction: .DepositAction, - recvDisabled: recvDisabled, - recvAction: .WithdrawAction) - } -} diff --git a/TalerWallet1/Views/Actions/PhaseView.swift b/TalerWallet1/Views/Actions/PhaseView.swift @@ -0,0 +1,79 @@ +/* + * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. + * See LICENSE.md + */ +/** + * @author Marc Stibane + */ +import SwiftUI + +struct PhaseView: View { + let stack: CallStack + let date: Date +// let phase: Int + let sendDisabled: Bool // can't send/deposit if wallet has no coins at all + let recvDisabled: Bool // can't receive/withdraw if wallet has no payments services + let depoDisabled: Bool + let wthdDisabled: Bool + +#if TALER_NIGHTLY + @State private var phaseCounter = -5 +#else + @State private var phaseCounter = -300 +#endif + + func phase(_ first: Bool) -> Int { + if phaseCounter >= 0 { + let myPhase = phaseCounter % 32 + if first { + return myPhase > 15 ? 0 : myPhase + } + return myPhase < 17 ? 0 : myPhase - 16 + } + return 0 + } + + var body: some View { + let sendTitle = String(localized: "SendButton_Short", defaultValue: "Send", + comment: "Abbreviation of button `Send (currency)´") + let requTitle = String(localized: "RequestButton_Short", defaultValue: "Request", + comment: "Abbreviation of button `Request (currency)´") + + /// [􁉇 Send ] [􁉅 Request] + TwoRowButtons(stack: stack.push(), + phase: phase(true), + sendTitle: sendTitle, + sendType: .peerPushDebit, + sendA11y: sendTitle, + recvTitle: requTitle, + recvType: .peerPullCredit, + recvA11y: requTitle, + sendDisabled: sendDisabled, + sendAction: .SendAction, + recvDisabled: recvDisabled, + recvAction: .RequestAction) + .accessibility(sortPriority: 1) // read this after maps + .onChange(of: date) { _ in + phaseCounter += 1 + } + + let depositTitle = String(localized: "DepositButton_Short", defaultValue: "Deposit", + comment: "Abbreviation of button `Deposit (currency)´") + let withdrawTitle = String(localized: "WithdrawButton_Short", defaultValue: "Withdraw", + comment: "Abbreviation of button `Withdraw (currency)´") + /// [􁾩Deposit] [􁾭 Withdraw] + TwoRowButtons(stack: stack.push(), + phase: phase(false), + sendTitle: depositTitle, + sendType: .deposit, + sendA11y: depositTitle, + recvTitle: withdrawTitle, + recvType: .withdrawal, + recvA11y: withdrawTitle, + sendDisabled: depoDisabled, + sendAction: .DepositAction, + recvDisabled: wthdDisabled, + recvAction: .WithdrawAction) + .accessibility(sortPriority: 0) // read this last + } +} diff --git a/TalerWallet1/Views/Actions/SendRequestV.swift b/TalerWallet1/Views/Actions/SendRequestV.swift @@ -1,33 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI - -/// [􁉇 Send ] [􁉅 Request] -struct SendRequestV: View { - let stack: CallStack - let sendDisabled: Bool // can't send/deposit if wallet has no coins at all - let recvDisabled: Bool // can't receive/withdraw if wallet has no payments services - - var body: some View { - let sendTitle = String(localized: "SendButton_Short", defaultValue: "Send", - comment: "Abbreviation of button `Send (currency)´") - let requTitle = String(localized: "RequestButton_Short", defaultValue: "Request", - comment: "Abbreviation of button `Request (currency)´") - TwoRowButtons(stack: stack.push(), - sendTitle: sendTitle, - sendType: .peerPushDebit, - sendA11y: sendTitle, - recvTitle: requTitle, - recvType: .peerPullCredit, - recvA11y: requTitle, - sendDisabled: sendDisabled, - sendAction: .SendAction, - recvDisabled: recvDisabled, - recvAction: .RequestAction) - } -} diff --git a/TalerWallet1/Views/Actions/TwoRowButtons.swift b/TalerWallet1/Views/Actions/TwoRowButtons.swift @@ -12,6 +12,7 @@ let LINELIMIT = 5 struct TypeButton: View { let stack: CallStack + let phase: Int let title: String let a11y: String var type: TransactionType @@ -36,12 +37,13 @@ struct TypeButton: View { #endif let disabledColor = WalletColors().gray2 let primaryAccent = WalletColors().primaryAccent - let badge = ButtonIconBadge(type: type, - foreColor: disabled ? disabledColor : primaryAccent, - done: false) + let icon = ButtonIconBadge(type: type, + phase: phase, + foreColor: disabled ? disabledColor : primaryAccent, + done: false) .talerFont(.title2) let hLayout = HStack { - badge + icon Spacer() Text(title) .fixedSize(horizontal: true, vertical: false) @@ -57,7 +59,7 @@ struct TypeButton: View { if fragCount > 1 { Text(fragments[0]) HStack { - badge + icon Spacer() Text(fragments[1]) .fixedSize(horizontal: true, vertical: false) @@ -83,7 +85,7 @@ struct TypeButton: View { Button(action: dismissAndPost) { if minimalistic { - badge + icon } else { if #available(iOS 16.4, *) { ViewThatFits(in: .horizontal) { @@ -105,6 +107,7 @@ struct TypeButton: View { // MARK: - struct TwoRowButtons: View { let stack: CallStack + let phase: Int? let sendTitle: String var sendType: TransactionType let sendA11y: String @@ -116,8 +119,9 @@ struct TwoRowButtons: View { let recvDisabled: Bool // can't receive/withdraw if wallet has no payments services let recvAction: NSNotification.Name - func sendButton(_ title: String) -> TypeButton { + func sendButton(_ title: String, _ phase: Int) -> TypeButton { TypeButton(stack: stack.push(), + phase: phase, title: title, a11y: sendA11y, type: sendType, @@ -125,8 +129,9 @@ struct TwoRowButtons: View { action: sendAction) } - func recvButton(_ title: String) -> TypeButton { + func recvButton(_ title: String, _ phase: Int) -> TypeButton { TypeButton(stack: stack.push(), + phase: phase, title: title, a11y: recvA11y, type: recvType, @@ -134,16 +139,28 @@ struct TwoRowButtons: View { action: recvAction) } + func phase(_ first: Bool) -> Int { + if let phase { + let myPhase = phase % 16 + if first { + return myPhase > 7 ? 0 : myPhase + } + return myPhase < 9 ? 0 : myPhase - 8 + } + return 0 + } + var body: some View { +// let _ = Self._printChanges() let hLayout = HStack(spacing: HSPACING) { // side by side, text in 1+ lines (\t -> \n) - sendButton(sendTitle.tabbed(oneLine: false)) - recvButton(recvTitle.tabbed(oneLine: false)) + sendButton(sendTitle.tabbed(oneLine: false), phase(true)) + recvButton(recvTitle.tabbed(oneLine: false), phase(false)) } let vLayout = VStack(alignment: .leading) { // one below the other, text in one line (\t -> " ") - sendButton(sendTitle.tabbed(oneLine: true)) - recvButton(recvTitle.tabbed(oneLine: true)) + sendButton(sendTitle.tabbed(oneLine: true), phase(true)) + recvButton(recvTitle.tabbed(oneLine: true), phase(false)) } if #available(iOS 16.4, *) { @@ -162,6 +179,7 @@ struct TwoRowButtons_Previews: PreviewProvider { static var previews: some View { List { TwoRowButtons(stack: CallStack("Preview"), + phase: nil, sendTitle: "Send " + TESTCURRENCY, sendType: .peerPushDebit, sendA11y: "Send " + TESTCURRENCY, @@ -174,6 +192,7 @@ struct TwoRowButtons_Previews: PreviewProvider { recvAction: .RequestAction) .listRowSeparator(.hidden) TwoRowButtons(stack: CallStack("Preview"), + phase: nil, sendTitle: "Send " + DEMOCURRENCY, sendType: .peerPushDebit, sendA11y: "Send " + DEMOCURRENCY, diff --git a/TalerWallet1/Views/HelperViews/IconBadge.swift b/TalerWallet1/Views/HelperViews/IconBadge.swift @@ -24,6 +24,9 @@ struct PendingIconBadge: View { foreColor: foreColor, shouldConfirm: shouldConfirm, needsKYC: needsKYC, + phase: 0, + firstImage: nil, + secondImage: nil, wideIcon: nil) } } @@ -78,22 +81,45 @@ struct TransactionIconBadge: View { foreColor: foreColor, shouldConfirm: shouldConfirm, needsKYC: needsKYC, + phase: 0, + firstImage: nil, + secondImage: nil, wideIcon: TransactionType.refund.icon()) // "arrowshape.turn.up.backward" is wider than all others } } // MARK: - struct ButtonIconBadge: View { - var type: TransactionType + let type: TransactionType + let phase: Int let foreColor:Color let done: Bool var body: some View { + let isSend = type.isSendCoins + let isRcve = type.isSendInvoice + let isDepo = type.isDeposit + let isWthd = type.isWithdrawal + let left = ICONNAME_PERSON_LEFT + ICONNAME_FILL + let right = ICONNAME_PERSON_RIGHT + let bottom = ICONNAME_PERSON_BOTTOM + ICONNAME_FILL + let top = ICONNAME_BANK + let firstImage = isSend ? Image(left) + : isRcve ? Image(right) + : isDepo ? Image(bottom) + : isWthd ? Image(top) : nil + let secondImage = isSend ? Image(right) + : isRcve ? Image(left) + : isDepo ? Image(top) + : isWthd ? Image(bottom) : nil IconBadge(image: type.icon(done), done: true, foreColor: foreColor, shouldConfirm: false, needsKYC: false, + phase: phase, + firstImage: firstImage, + secondImage: secondImage, wideIcon: TransactionType.peerPushDebit.icon()) // button is send/receive/withdraw/deposit, never payment or refund } @@ -105,18 +131,45 @@ struct IconBadge: View { let foreColor:Color let shouldConfirm: Bool let needsKYC: Bool + let phase: Int + let firstImage: Image? + let secondImage: Image? let wideIcon: Image? // cheating: ZStack with widest icon to ensure all have the same width // TODO: EqualIconWidth... @ScaledMetric var spacing = 6 // relative to fontSize + @State private var showFirst = false + @State private var showSecond = false + @State private var showImage = true var body: some View { +// let _ = Self._printChanges() HStack(alignment: .top, spacing: -spacing) { ZStack { if let wideIcon { wideIcon.foregroundColor(.clear) } - image.foregroundColor(foreColor) + if let firstImage, let secondImage { + let duration = 0.66 + ZStack { + image.opacity(showImage ? 1 : 0) + .animation(.easeInOut(duration: duration), value: showImage) + firstImage.opacity(showFirst ? 1 : 0) + .animation(.easeOut(duration: duration), value: showFirst) + secondImage.opacity(showSecond ? 1 : 0) + .animation(.easeIn(duration: duration), value: showSecond) + } .foregroundColor(foreColor) + .onChange(of: phase) { phaseVal in + switch phaseVal { + case 0, 4: showFirst = false; showSecond = false; showImage = true + case 2, 3: showFirst = true; showSecond = false; showImage = false + case 5, 6: showFirst = false; showSecond = true; showImage = false + default: showFirst = false; showSecond = false; showImage = false + } + } + } else { + image.foregroundColor(foreColor) + } } // ZStack centers the main icon, so the badge will always be at the same position let badgeName = needsKYC ? NEEDS_KYC