commit 95996776bd925d02735039c7fc61ad10e22ddb62
parent b2a3273a4ea0976bda2b29a3c6d2be182c91997f
Author: Marc Stibane <marc@taler.net>
Date: Mon, 14 Jul 2025 20:16:44 +0200
History animations
Diffstat:
6 files changed, 194 insertions(+), 97 deletions(-)
diff --git a/TalerWallet1/Views/OIM/OIMEditView.swift b/TalerWallet1/Views/OIM/OIMEditView.swift
@@ -92,9 +92,11 @@ struct OIMEditView: View {
) {
ZStack(alignment: .top) {
VStack {
-// OIMtitleView(cash: cash, amount: availRest, isSending: true, secondAmount: amount)
-// OIMtitleView(cash: cash, amount: available, isSending: true, secondAmount: amount2)
- OIMtitleView(cash: cash, amount: available, isSending: true, secondAmount: amountToTransfer)
+ OIMtitleView(cash: cash,
+ amount: available,
+ isSending: true,
+ history: false,
+ secondAmount: amountToTransfer)
Spacer()
OIMlineView(stack: stack.push(),
cash: cash,
diff --git a/TalerWallet1/Views/OIM/OIMSubjectView.swift b/TalerWallet1/Views/OIM/OIMSubjectView.swift
@@ -95,7 +95,11 @@ struct OIMSubjectView: View {
action: gotAction
) {
VStack {
- OIMtitleView(cash: cash, amount: available, isSending: true, secondAmount: amountToTransfer)
+ OIMtitleView(cash: cash,
+ amount: available,
+ isSending: true,
+ history: false,
+ secondAmount: amountToTransfer)
Spacer()
GoalsHStack(goals: goals1, selectedGoal: $selectedGoal)
.opacity(appeared ? 1.0 : 0.01)
diff --git a/TalerWallet1/Views/OIM/OIMView.swift b/TalerWallet1/Views/OIM/OIMView.swift
@@ -15,6 +15,8 @@ let OIMACTION = "OIMaction"
let OIMACTION2 = "OIMaction2"
let OIMNUMBER = "OIMnumber"
let OIMCHEST = "OIMchest"
+let OIMBACK = "OIMback"
+let OIMSIDE = "OIMside"
// MARK: -
struct OIMnavBack<Content: View>: View {
@@ -64,6 +66,7 @@ struct OIMtitleView: View {
let cash: OIMcash
let amount: Amount?
let isSending: Bool
+ let history: Bool
let secondAmount: Amount?
@EnvironmentObject private var wrapper: NamespaceWrapper
@@ -78,24 +81,44 @@ struct OIMtitleView: View {
.matchedGeometryEffect(id: OIMNUMBER, in: wrapper.namespace, isSource: true)
.accessibilityHidden(true)
- OIMamountV(amount: amount, currencyName: cash.currency.noteBase)
-
- if isSending {
+ if history {
Spacer()
- OIMamountV(amount: secondAmount, currencyName: cash.currency.noteBase)
}
+ OIMamountV(amount: amount, currencyName: cash.currency.noteBase)
+ if !history {
+ if isSending {
+ Spacer()
+ OIMamountV(amount: secondAmount, currencyName: cash.currency.noteBase)
+ }
- OIMactionButton(type: .sendP2P, isFinal: false, action: nil)
- .frame(width: OIMactionSize, height: OIMbuttonSize)
- .disabled(true)
- .opacity(0.01)
- .matchedGeometryEffect(id: OIMACTION, in: wrapper.namespace, isSource: true)
- .accessibilityHidden(true)
-
+ OIMactionButton(type: .sendP2P, isFinal: false, action: nil)
+ .frame(width: OIMactionSize, height: OIMbuttonSize)
+ .disabled(true)
+ .opacity(0.01)
+ .matchedGeometryEffect(id: OIMACTION, in: wrapper.namespace, isSource: true)
+ .accessibilityHidden(true)
+ }
}
}
}
// MARK: -
+enum OIMViewState {
+ case chestsClosed
+ case chestClosing
+ case chestOpenTapped
+ case chestIsOpen
+
+ case sendTapped
+ case sending
+
+ case requestTapped
+ case requesting
+
+ case balanceTapped
+ case historyShown
+ case historyTapped
+}
+
@available(iOS 16.4, *)
struct OIMView: View {
let stack: CallStack
@@ -112,21 +135,28 @@ struct OIMView: View {
@State private var tappedVal: UInt64 = 0
@State private var available: Amount? = nil
@State private var chestOpen: Int? = nil
- @State private var showingActions = false // set true after user opened a chest, set false when choosing an action
- @State private var sending = false // after user tapped on Send or on the money
+ @State private var viewState: OIMViewState = .chestsClosed
+// @State private var showingActions = false // set true after user opened a chest, set false when choosing an action
+// @State private var sending = false // after user tapped on Send or on the money
@State private var closing = false // after user tapped on the open chest
func noAction() { }
- func sendAction() {
+ func requestTapped() {
+
+ }
+
+ func sendTapped() {
withAnimation(.basicFast) {
- showingActions = false //
+// showingActions = false //
+ viewState = .sendTapped
}
-// let delay =
- cash.moveDown() // cash animates itself
+// let delay = cash.moveDown() // cash animates itself
+ cash.flyOneByOne(to: .drawer)
// withAnimation(.basic1.delay(delay + 0.5)) {
withAnimation(.basic1.delay(0.6)) {
- sending = true // blends in the missing denominations
+// sending = true // blends in the missing denominations
+ viewState = .sending
}
// DispatchQueue.main.asyncAfter(deadline: .now() + delay + 1) { // cash.delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
@@ -139,19 +169,20 @@ struct OIMView: View {
}
}
- func closeAnimated() {
+ func closeChest() {
if !closing {
closing = true
- let duration = fastAnimations ? 0.6 : 1.1
- let delay = cash.backToChest(duration)
+ viewState = .chestClosing
+ let delay = cash.flyOneByOne(to: .curve) // back to chest...
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
- print("closeAnimated", delay)
+ print("closeChest", delay)
withAnimation(.basic1) {
chestOpen = nil
selectedBalance = nil
selectedIndex = nil
available = nil
- showingActions = false
+// showingActions = false
+ viewState = .chestsClosed
}
closing = false
}
@@ -171,8 +202,10 @@ struct OIMView: View {
duration = fastAnimations ? 0.6 : 1.1
initial = 0.1
#endif
+ viewState = .chestOpenTapped
withAnimation(.basic1) {
chestOpen = index
+ viewState = .chestIsOpen
selectedIndex = index
cash.currency = index == 0 ? OIMeuros : OIMleones
selectedBalance = balance
@@ -184,6 +217,30 @@ struct OIMView: View {
}
}
+ func closeHistory() {
+ withAnimation(.basic1) {
+ viewState = .historyTapped
+ }
+ let delay = cash.flyOneByOne(to: .idle) // back to center
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ print("closeHistory", delay)
+ withAnimation(.basic1) {
+ viewState = .chestIsOpen
+ }
+ }
+ }
+
+ func openHistory() {
+ viewState = .balanceTapped
+ let delay = cash.flyOneByOne(to: .history, true)
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ print("openHistory", delay)
+ withAnimation(.basic1) {
+ viewState = .historyShown
+ }
+ }
+ }
+
var body: some View {
var debugTick = 0
// let _ = Self._printChanges()
@@ -192,58 +249,69 @@ struct OIMView: View {
!available.isZero
} else { false }
let topButtons = HStack(alignment: .top) {
- QRButton(hideTitle: true) {
- qrButtonTapped = true
- }
- .opacity(sending ? 0.01 : 1.0)
+ if chestOpen == nil {
+ let showQR = viewState == .chestsClosed
+ QRButton(hideTitle: true) {
+ qrButtonTapped = true
+ }
+ .opacity(showQR ? 1.0 : 0.01)
.frame(width: OIMbuttonSize, height: OIMbuttonSize)
- .matchedGeometryEffect(id: "OIMback", in: wrapper.namespace, isSource: true)
+ .matchedGeometryEffect(id: OIMBACK, in: wrapper.namespace, isSource: true)
+ } else {
+ let showRequest = viewState == .chestIsOpen
+ OIMactionButton(type: .requestP2P, isFinal: false, action: requestTapped)
+ .frame(width: OIMbuttonSize, height: OIMbuttonSize)
+ .opacity(showRequest ? 1.0 : 0.01)
+ }
Spacer()
-// OIMsendButton(isGoal: false, isFinal: false, enabled: enabled, action: sendAction)
-// .frame(width: OIMbuttonSize, height: OIMbuttonSize)
+ let showSend = viewState == .chestIsOpen
+ OIMactionButton(type: .sendP2P, isFinal: false, action: sendTapped)
+ .frame(width: OIMbuttonSize, height: OIMbuttonSize)
+ .opacity(showSend ? 1.0 : 0.01)
}
let maxAvailable = cash.max(available: available?.centValue ?? 0)
// let _ = print("maxAvailable", maxAvailable)
- let botButtons = HStack() {
- let buttons: [OIMactions] = [.deposit, .withdrawal, .requestP2P, .sendP2P].shuffled()
+ let sidePosition = HStack {
Spacer()
- ForEach(buttons, id: \.self) { button in
- let action = switch button {
- case .sendP2P: sendAction
- case .withdrawal: noAction
- case .deposit: noAction
- case .requestP2P: noAction
- }
- OIMactionButton(type: button, isFinal: false, action: action)
- .frame(width: OIMactionSize, height: OIMactionSize)
- Spacer()
- }
+ Color.clear
+ .frame(width: 80, height: 80)
+ .matchedGeometryEffect(id: OIMSIDE, in: wrapper.namespace, isSource: true)
}
-
OIMbackground() {
ZStack(alignment: .top) {
topButtons
VStack {
- OIMtitleView(cash: cash, amount: available, isSending: sending, secondAmount: nil)
+ let isSending = viewState == .sending
+ OIMtitleView(cash: cash,
+ amount: available,
+ isSending: isSending,
+ history: viewState == .historyShown,
+ secondAmount: nil)
Spacer()
let isOpen = chestOpen != nil
- OIMlineView(stack: stack.push(),
- cash: cash,
- amountVal: $availableVal,
- tappedVal: $tappedVal,
- canEdit: false)
- .opacity(isOpen ? 1.0 : 0.01)
- .scaleEffect(showingActions ? 0.6 : 1.0)
- .onTapGesture {
- withAnimation(.basic1) {
- showingActions.toggle()
+ ZStack {
+ sidePosition
+// let scaleMoney = viewState == .chestIsOpen
+ OIMlineView(stack: stack.push(),
+ cash: cash,
+ amountVal: $availableVal,
+ tappedVal: $tappedVal,
+ canEdit: false)
+ .opacity(isOpen ? 1.0 : 0.01)
+// .scaleEffect(scaleMoney ? 0.6 : 1.0)
+ .onTapGesture {
+ if viewState == .historyShown {
+ closeHistory()
+ } else {
+ openHistory()
+ }
}
- }
+ }
Spacer()
- botButtons
- .opacity(showingActions ? 1.0 : 0.01)
+// botButtons
+// .opacity(showingActions ? 1.0 : 0.01)
} // title, money, buttons
VStack {
@@ -261,7 +329,7 @@ struct OIMView: View {
ZStack {
OIMbalanceButton(isOpen: itsMe, isSierra: index > 0, isFinal: false) {
if itsMe {
- closeAnimated()
+ closeChest()
} else {
openChest(index, balance)
}
@@ -269,7 +337,7 @@ struct OIMView: View {
.frame(width: size, height: size)
.zIndex(itsMe ? 3 : 0)
.opacity((isClosed || itsMe) ? 1.0 : 0.01)
- .matchedGeometryEffect(id: itsMe ? OIMNUMBER // (sending ? "OIMback" : OIMNUMBER)
+ .matchedGeometryEffect(id: itsMe ? OIMNUMBER
: String(index),
in: wrapper.namespace, isSource: false)
.frame(width: size, height: size)
@@ -284,6 +352,7 @@ struct OIMView: View {
VStack {
Spacer()
+ let showDrawer = viewState == .sending
OIMcurrencyDrawer(stack: stack.push(),
cash: cash,
availableVal: $availableVal,
@@ -294,18 +363,18 @@ struct OIMView: View {
.padding(.horizontal, 5)
.ignoresSafeArea(edges: .horizontal)
.scrollDisabled(true)
- .opacity(sending ? 1.0 : 0.01)
- } // for matching positions of money
+ .opacity(showDrawer ? 1.0 : 0.01)
+ } // for matching positions of money in the drawer
}
}
.onAppear {
- showingActions = false
+// showingActions = false
if (chestOpen != nil) {
let balance = controller.balances[0]
available = balance.available
availableVal = available?.centValue ?? 0
cash.update2(availableVal) // set cash to available
- showingActions = true
+// showingActions = true
} else {
availableVal = 0
cash.update2(availableVal) // set cash to available
@@ -314,7 +383,8 @@ struct OIMView: View {
}
.onDisappear {
cash.moveBack()
- sending = false
+// sending = false
+ viewState = (chestOpen != nil) ? .chestIsOpen : .chestsClosed
}
}
}
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -14,13 +14,16 @@ let MAXSTACK = 4
enum FundState: Int {
case idle
- case returning // user tapped to remove
+ case removing // user tapped to remove
+ case drawer
case shouldFly // + button tapped
case isFlying // after onAppear
+
case chestOpening // chest tapped, money flies out
case reveal // move (invisible) together with the chest to the top
case curve // fly out of the chest in a curve to the topRight corner
+ case history // pile up for history
case position // position to move to
case moving // move to position
@@ -29,12 +32,14 @@ enum FundState: Int {
case arriving // keep flipped image, move to final position
case mutating // flip back with disbled transition
- var returning: Bool { self == .returning }
+ var removing: Bool { self == .removing }
+ var drawer: Bool { self == .drawer }
var shouldFly: Bool { self == .shouldFly }
var isFlying: Bool { self == .isFlying || self == .reveal || self == .curve }
var chestOpening: Bool { self == .chestOpening }
var reveal: Bool { self == .reveal }
var curve: Bool { self == .curve }
+ var history: Bool { self == .history }
var position: Bool { self == .position }
var morphing: Bool { self == .morphing }
var moving: Bool { self == .moving }
@@ -53,11 +58,13 @@ public struct OIMfund: Identifiable, Equatable, Hashable, Sendable {
var flippedVal: UInt64? // the value to morph into - layout.sortByValue will take this instead of value
var outValue: UInt64? // if this is set it determines the image (used for flipping)
var targetID: String { // match sourceID for SwiftUI animations
- state.chestOpening ? OIMCHEST + String(currencyIndex)
- : state.reveal ? OIMNUMBER
- : state.curve ? OIMACTION
- : String(state.shouldFly || state.returning ? -Int(value) // match currencyScroller
- : id)
+ let isDrawer = state.shouldFly || state.drawer || state.removing
+ return state.chestOpening ? OIMCHEST + String(currencyIndex)
+ : state.reveal ? OIMNUMBER
+ : state.curve ? OIMACTION
+ : state.history ? OIMSIDE
+ : String(isDrawer ? -Int(value) // match currencyDrawer
+ : id)
}
var shouldFly2: Bool { state.shouldFly || state.chestOpening }
var isFlying2: Bool { state.isFlying }
@@ -350,7 +357,7 @@ final class OIMcash: ObservableObject, Sendable {
func removeCash(id: Int, value: UInt64) {
if let index = funds.firstIndex(where: { $0.id == id && $0.value == value }) {
funds.remove(at: index)
- } else if let index = funds.lastIndex(where: { $0.value == value && $0.state.returning }) {
+ } else if let index = funds.lastIndex(where: { $0.value == value && $0.state.removing }) {
funds.remove(at: index)
} else if let index = funds.firstIndex(where: { $0.value == value }) {
funds.remove(at: index)
@@ -377,7 +384,7 @@ final class OIMcash: ObservableObject, Sendable {
for var fund in sorted {
// withAnimation(.easeOut1.delay(counter * 0.1)) {
withAnimation(.easeOut1) {
- fund.state = .returning
+ fund.state = .drawer
self.updateFund(fund)
}
if lastVal != fund.value {
@@ -390,7 +397,7 @@ final class OIMcash: ObservableObject, Sendable {
func moveBack() {
for var fund in funds {
- if fund.state == .returning {
+ if fund.state == .drawer {
fund.state = .idle
self.updateFund(fund)
}
@@ -407,39 +414,49 @@ final class OIMcash: ObservableObject, Sendable {
}
}
- func flyToChest(_ index: Int, after delay: TimeInterval) {
+ func flyToTarget(target: FundState, index: Int, after delay: TimeInterval) {
let interval = fastAnimations ? 0.2 : 0.4
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
var fund = self.funds[index]
withAnimation(.move1) {
- fund.state = .curve
+ fund.state = target
self.updateFund(fund)
}
- print("backToChest", fund.id, fund.value, delay)
- DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
- withAnimation(.move1) {
- fund.state = .reveal
- self.updateFund(fund)
- }
+ print("flyToTarget", target, fund.id, fund.value, delay)
+ if target == .curve {
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
withAnimation(.move1) {
- do {
- self.funds.remove(at: index)
- } catch {}
+ fund.state = .reveal
+ self.updateFund(fund)
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
+ withAnimation(.move1) {
+ do {
+ self.funds.remove(at: index)
+ } catch {}
+ }
}
}
- }
+ } // curve
}
}
- func backToChest(_ duration: TimeInterval) -> TimeInterval {
+ func flyOneByOne(to target: FundState,
+ _ increasing: Bool = false,
+ _ duration: TimeInterval? = nil
+ ) -> TimeInterval {
var count = funds.count
var initial: TimeInterval = 0.01
- let interval = interval(count: count, duration: duration, initial: initial)
-
+ let interval = interval(count: count,
+ duration: duration ?? (fastAnimations ? 0.6 : 1.1),
+ initial: initial)
+ var index = 0
while count > 0 { // for each fund, small to high
count -= 1
- flyToChest(count, after: initial)
+ flyToTarget(target: target,
+ index: increasing ? index : count,
+ after: initial)
+ index += 1
initial += interval
}
return initial - interval + (fastAnimations ? 0.1 : 0.2)
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -109,7 +109,7 @@ struct OIMlayoutView: View {
var fund = funds[index]
symLog.log("*** fly back:\(fund.value) \(fundID)")
withAnimation(.move1) {
- fund.state = .returning
+ fund.state = .removing
cash.updateFund(fund)
}
DispatchQueue.main.asyncAfter(deadline: .now() + cash.flyDelay) {
diff --git a/TalerWallet1/Views/OIM/OIMp2pReceiveView.swift b/TalerWallet1/Views/OIM/OIMp2pReceiveView.swift
@@ -44,7 +44,11 @@ struct OIMp2pReceiveView: View {
let goal = terms.icon_id
let effective = terms.amount
VStack {
- OIMtitleView(cash: cash, amount: effective, isSending: false, secondAmount: nil)
+ OIMtitleView(cash: cash,
+ amount: effective,
+ isSending: false,
+ history: false,
+ secondAmount: nil)
Spacer()
if let goal {
Image(goal)