commit 7fadf6fb30ab53d59141434bbd06f62070a050a7
parent af55ce6ca58dac792823a6d2161b73cdc53fd485
Author: Marc Stibane <marc@taler.net>
Date: Tue, 7 Jul 2026 20:19:21 +0200
Phase animation for actions
Diffstat:
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