commit 3270843c61095f5fc6043c30d087956295767bd8
parent a92dc23dcc3861ac0fbec1ea2ecf0d1d727ae5da
Author: Marc Stibane <marc@taler.net>
Date: Tue, 8 Jul 2025 17:34:28 +0200
Chest animation
Diffstat:
8 files changed, 202 insertions(+), 92 deletions(-)
diff --git a/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift
@@ -44,8 +44,10 @@ struct RequestPayment: View {
self._summary = summary
self._iconID = iconID
//#if OIM
- let currency = selectedIndex == 1 ? OIMleones : OIMeuros
- self._cash = StateObject(wrappedValue: { OIMcash(currency) }())
+ let index = selectedIndex ?? 0
+ let currency = index == 0 ? OIMeuros : OIMleones
+ let oimCash = OIMcash(currency, currencyIndex: (index > 0) ? 1 : 0)
+ self._cash = StateObject(wrappedValue: { oimCash }())
//#endif
}
diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift
@@ -46,8 +46,10 @@ struct SendAmountV: View {
self._summary = summary
self._iconID = iconID
//#if OIM
- let currency = selectedIndex == 1 ? OIMleones : OIMeuros
- self._cash = StateObject(wrappedValue: { OIMcash(currency) }())
+ let index = selectedIndex ?? 0
+ let currency = index == 0 ? OIMeuros : OIMleones
+ let oimCash = OIMcash(currency, currencyIndex: (index > 0) ? 1 : 0)
+ self._cash = StateObject(wrappedValue: { oimCash }())
//#endif
}
diff --git a/TalerWallet1/Views/OIM/OIMEditView.swift b/TalerWallet1/Views/OIM/OIMEditView.swift
@@ -142,7 +142,7 @@ struct OIMEditView: View {
amountVal = amountToTransfer.centValue
availableVal = isAvailable
// print("OIMEditView.task", availableVal, amountVal)
- cash.update2(amountVal)
+ cash.update2(amountVal, state: .idle)
}
.onAppear {
// print("OIMEditView.onAppear", availableVal, amountVal)
diff --git a/TalerWallet1/Views/OIM/OIMView.swift b/TalerWallet1/Views/OIM/OIMView.swift
@@ -11,6 +11,11 @@ import taler_swift
let OIMbuttonSize = 80.0
let OIMactionSize = 120.0
+let OIMACTION = "OIMaction"
+let OIMACTION2 = "OIMaction2"
+let OIMNUMBER = "OIMnumber"
+let OIMCHEST = "OIMchest"
+
// MARK: -
struct OIMnavBack<Content: View>: View {
let stack: CallStack
@@ -45,7 +50,7 @@ struct OIMnavBack<Content: View>: View {
isFinal: isFinal,
action: amountIsZero ? nil : action)
.frame(width: OIMactionSize, height: OIMbuttonSize)
- .matchedGeometryEffect(id: isSending ? "OIMaction" : "OIMaction2",
+ .matchedGeometryEffect(id: isSending ? OIMACTION : OIMACTION2,
in: wrapper.namespace, isSource: false)
}
}
@@ -70,7 +75,7 @@ struct OIMtitleView: View {
.frame(width: OIMbuttonSize, height: OIMbuttonSize)
.disabled(true)
.opacity(0.01)
- .matchedGeometryEffect(id: "OIMnumber", in: wrapper.namespace, isSource: true)
+ .matchedGeometryEffect(id: OIMNUMBER, in: wrapper.namespace, isSource: true)
.accessibilityHidden(true)
OIMamountV(amount: amount, currencyName: cash.currency.noteBase)
@@ -84,7 +89,7 @@ struct OIMtitleView: View {
.frame(width: OIMactionSize, height: OIMbuttonSize)
.disabled(true)
.opacity(0.01)
- .matchedGeometryEffect(id: "OIMaction", in: wrapper.namespace, isSource: true)
+ .matchedGeometryEffect(id: OIMACTION, in: wrapper.namespace, isSource: true)
.accessibilityHidden(true)
}
@@ -144,16 +149,29 @@ struct OIMView: View {
}
func openChest(_ index: Int, _ balance: Balance) {
- chestOpen = index
- selectedIndex = index
- cash.currency = index == 0 ? OIMeuros : OIMleones
- selectedBalance = balance
- available = balance.available
-
- availableVal = available?.centValue ?? 0
- cash.update2(availableVal, 0.2) // set cash to available
- let maxAvailable = cash.max(available: availableVal)
- print("OIMView.task availableVal", availableVal, maxAvailable)
+ cash.clearFunds()
+ print("❗️openChest❗️")
+ let duration: TimeInterval
+ let initial: TimeInterval
+#if DEBUG
+ duration = debugAnimations ? 2.5 :
+ fastAnimations ? 0.6 : 1.1
+ initial = debugAnimations ? 1.0 : 0.1
+#else
+ duration = fastAnimations ? 0.6 : 1.1
+ initial = 0.1
+#endif
+ withAnimation(.basic1) {
+ chestOpen = index
+ selectedIndex = index
+ cash.currency = index == 0 ? OIMeuros : OIMleones
+ selectedBalance = balance
+ available = balance.available
+ availableVal = balance.available.centValue
+ cash.update2(availableVal, state: .chestOpening, duration, initial) // set cash to available
+ let maxAvailable = cash.max(available: availableVal)
+ print("OIMView.openChest availableVal", availableVal, maxAvailable)
+ }
}
var body: some View {
@@ -230,22 +248,25 @@ struct OIMView: View {
let itsMe = chestOpen == index
let isClosed = chestOpen == nil
let size = isClosed ? 160.0 : OIMbuttonSize
- OIMbalanceButton(isOpen: itsMe, isSierra: index > 0, isFinal: false) {
- if itsMe {
- closeAnimated()
- } else {
- withAnimation(.basic1) {
+ ZStack {
+ OIMbalanceButton(isOpen: itsMe, isSierra: index > 0, isFinal: false) {
+ if itsMe {
+ closeAnimated()
+ } else {
openChest(index, balance)
}
}
+ .frame(width: size, height: size)
+ .zIndex(itsMe ? 3 : 0)
+ .opacity((isClosed || itsMe) ? 1.0 : 0.01)
+ .matchedGeometryEffect(id: itsMe ? OIMNUMBER // (sending ? "OIMback" : OIMNUMBER)
+ : String(index),
+ in: wrapper.namespace, isSource: false)
+ .frame(width: size, height: size)
+ Color.clear
+ .frame(width: 40, height: 40)
+ .matchedGeometryEffect(id: OIMCHEST + String(index), in: wrapper.namespace, isSource: true)
}
- .frame(width: size, height: size)
- .zIndex(itsMe ? 3 : 0)
- .opacity((isClosed || itsMe) ? 1.0 : 0.01)
- .matchedGeometryEffect(id: itsMe ? "OIMnumber" // (sending ? "OIMback" : "OIMnumber")
- : String(index),
- in: wrapper.namespace, isSource: false)
- .frame(width: size, height: size)
}
}
Spacer()
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -18,6 +18,9 @@ enum FundState: Int {
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 position // position to move to
case moving // move to position
@@ -28,7 +31,10 @@ enum FundState: Int {
var returning: Bool { self == .returning }
var shouldFly: Bool { self == .shouldFly }
- var isFlying: Bool { self == .isFlying }
+ 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 position: Bool { self == .position }
var morphing: Bool { self == .morphing }
var moving: Bool { self == .moving }
@@ -39,37 +45,56 @@ enum FundState: Int {
/// data structure for a cash item on the table
public struct OIMfund: Identifiable, Equatable, Hashable, Sendable {
public let id: Int // support multiple funds with the same value
- var state: FundState
var value: UInt64 // can be morphed
+ var currencyIndex: Int
+ var state: FundState
+ var delay: TimeInterval
var morphTicker: Int? // `semaphore´ to reserve a fund - don't morph this twice concurrently
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 {
- String(state.shouldFly || state.returning ? -Int(value) // match sourceID
- : id)
+ 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)
+ }
+ var shouldFly2: Bool { state.shouldFly || state.chestOpening }
+ var isFlying2: Bool { state.isFlying }
+
+ init(id: Int, value: UInt64, currencyIndex: Int,
+ state: FundState = .idle, delay: TimeInterval = 0,
+ morphTicker: Int? = nil, flippedVal: UInt64? = nil, outValue: UInt64? = nil
+ ) {
+ self.id = id
+ self.value = value
+ self.currencyIndex = currencyIndex
+ self.state = state
+ self.delay = delay
+ self.morphTicker = morphTicker
+ self.flippedVal = flippedVal
+ self.outValue = outValue
}
- var shouldFly: Bool { state.shouldFly }
- var isFlying: Bool { state.isFlying }
-// var isFlipping: Bool { state.flipping }
-
}
public typealias OIMfunds = [OIMfund]
// MARK: -
final class OIMcash: ObservableObject, Sendable {
- private let symLog = SymLogV(0)
+ private let symLog = SymLogV()
private let logger = Logger(subsystem: "net.taler.gnu", category: "OIMcash")
private var ticker = 0 // increment each time a fund is added (or melted from others)
// let semaphore = AsyncSemaphore(value: 1)
var currency: OIMcurrency
+ var currencyIndex: Int
@Published var funds: OIMfunds = []
- init(_ currency: OIMcurrency? = nil) {
+ init(_ currency: OIMcurrency? = nil, currencyIndex: Int = 0) {
if let currency {
self.currency = currency
+ self.currencyIndex = currencyIndex
} else {
self.currency = OIMeuros
+ self.currencyIndex = 0
}
}
@@ -87,7 +112,7 @@ final class OIMcash: ObservableObject, Sendable {
return 0
}
- var delay: Double {
+ var flyDelay: Double { // time
#if DEBUG
if debugAnimations { return 5.0 }
// return fastAnimations ? 5 : 1.0
@@ -95,6 +120,9 @@ final class OIMcash: ObservableObject, Sendable {
return fastAnimations ? 0.4 : 0.8
}
+ var curveDelay: Double {
+ flyDelay * 2 / 3
+ }
func sortByValue() -> OIMfunds {
/// sorts ASCENDING but keeps the order of the IDs for identical values
@@ -115,7 +143,7 @@ final class OIMcash: ObservableObject, Sendable {
for fund in sorted {
let value = fund.value
let state = fund.state
- if fund.shouldFly || fund.isFlying { // flying can not go on the stack
+ if fund.shouldFly2 || fund.isFlying2 { // flying can not go on the stack
lastValue = 0 // let the next subview...
stackIndex = 0 // ...start a new stack
} else if lastValue != value { // different value?
@@ -193,7 +221,7 @@ final class OIMcash: ObservableObject, Sendable {
}
/// Stage 2: After arriving at the position, without animation hide all outValues (except our anchor)
- DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + flyDelay) {
for var delFund in toDelete {
self.symLog.log(">>hide \(delFund.id)")
delFund.state = .hiding // make invisible
@@ -207,7 +235,7 @@ final class OIMcash: ObservableObject, Sendable {
self.symLog.log(">>flipped \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
/// Stage 4: move inValues to final position, delete outValues from funds array
- DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + self.flyDelay) {
morphFund.state = .arriving
morphFund.value = inValue // stack position
morphFund.morphTicker = nil // release `semaphore´
@@ -230,7 +258,7 @@ final class OIMcash: ObservableObject, Sendable {
}
/// Stage 5: transmute morphFund
- DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + self.flyDelay) {
morphFund.state = .mutating
// morphFund.state = .idle
morphFund.outValue = nil
@@ -309,10 +337,12 @@ final class OIMcash: ObservableObject, Sendable {
return funds.filter { $0.value <= firstCoinVal }
}
+ @discardableResult
func addCash(value: UInt64, _ newState: FundState = .shouldFly, _ flippedVal: UInt64? = nil) -> OIMfund {
let myTicker = ticker; ticker += 1 // TODO: atomic increment!
- let fund = OIMfund(id: myTicker, state: newState, value: value, flippedVal: flippedVal)
- symLog.log(">>adding \(value) \(fund.id)")
+ let fund = OIMfund(id: myTicker, value: value, currencyIndex: currencyIndex,
+ state: newState, flippedVal: flippedVal)
+ symLog.log(">>adding \(value) \(fund.id) \(newState)")
funds.append(fund)
return fund
}
@@ -367,24 +397,47 @@ final class OIMcash: ObservableObject, Sendable {
}
}
- func update2(_ intVal: UInt64, _ duration: TimeInterval = 0, _ initial: TimeInterval = 0) {
- let result = currency.notesCoins(intVal)
- let chng1 = update1(result.0, currencyNotesCoins: currency.bankNotes, duration, initial)
- let chng2 = update1(result.1, currencyNotesCoins: currency.bankCoins, duration, initial)
+ func clearFunds() {
+ funds = []
+ }
+
+ func countDosh(_ dosh: OIMdenominations) -> UInt64 {
+ dosh.reduce(0) { x, y in
+ x + y
+ }
+ }
+
+ func interval(count: UInt64,
+ duration: TimeInterval,
+ initial: TimeInterval = 0
+ ) -> TimeInterval {
+ (duration <= initial) || (count == 0) ? 0
+ : (duration - initial) / Double(count)
+ }
+
+ func update2(_ intVal: UInt64,
+ state: FundState = .idle,
+ _ duration: TimeInterval = 0,
+ _ initial: TimeInterval = 0) {
+ // optimize/rebuild funds
+ let dosh = currency.notesCoins(intVal)
+ let count = countDosh(dosh.0) + countDosh(dosh.1)
+ let interval = interval(count: count, duration: duration, initial: initial)
+ let delay = update1(dosh.0, denominations: currency.bankNotes, state: state, count, interval, initial)
+ update1(dosh.1, denominations: currency.bankCoins, state: state, count, interval, delay)
}
+ @discardableResult
func update1(_ notesCoins: OIMdenominations,
- currencyNotesCoins: OIMdenominations,
- _ duration: TimeInterval = 0,
- _ initial: TimeInterval = 0) -> Bool {
+ denominations: OIMdenominations,
+ state: FundState = .idle,
+ _ count: UInt64,
+ _ interval: TimeInterval = 0,
+ _ initial: TimeInterval = 0) -> TimeInterval {
var array = funds
var changed = false
var accumulatedDelay = initial
- let count = notesCoins.reduce(0) { x, y in
- x + y
- }
- let interval: TimeInterval = (duration <= initial) || (count == 0) ? 0 : (duration - initial) / Double(count)
- for (index, value) in currencyNotesCoins.enumerated() {
+ for (index, value) in denominations.enumerated() {
let wanted = notesCoins[index] // number of notes which should be shown
var shownNotes = array.filter { $0.value == value }
var count = shownNotes.count
@@ -399,24 +452,29 @@ final class OIMcash: ObservableObject, Sendable {
count -= 1
}
while count < wanted {
- let note = OIMfund(id: ticker, state: .idle, value: value, flippedVal: nil)
- ticker += 1
- symLog.log("update: add \(note.value), \(note.id)")
- if duration > 0 {
- withAnimation(.move1.delay(accumulatedDelay)) {
- array.append(note)
- }
+// if interval == 0 { // add all at once
+ let fund = OIMfund(id: ticker, value: value,
+ currencyIndex: currencyIndex, state: state,
+ delay: accumulatedDelay)
+ ticker += 1
+ symLog.log("update: add \(fund.value), \(fund.id)")
+ array.append(fund)
+ changed = true
+// } else { // add each delayed
+// DispatchQueue.main.asyncAfter(deadline: .now() + accumulatedDelay) {
+// withAnimation(.move1) {
+// self.addCash(value: value, state)
+// return
+// }
+// }
accumulatedDelay += interval
- } else {
- array.append(note)
- }
- changed = true
+// }
count += 1
}
}
if changed {
funds = array
}
- return changed
+ return accumulatedDelay
}
}
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyButton.swift b/TalerWallet1/Views/OIM/OIMcurrencyButton.swift
@@ -117,6 +117,7 @@ struct OIMcurrencyButton: View {
let currency: OIMcurrency
let availableVal: UInt64
let canEdit: Bool
+ let isDrawer: Bool // debugging - button in the drawer
var pct: CGFloat
let action: () -> Void
@@ -150,6 +151,11 @@ struct OIMcurrencyButton: View {
: 0.5
// checkDisabled = true // TODO: don't run twice
let unavailable = availableVal < value
+ if isDrawer && value == 2000 {
+ if unavailable {
+ let _ = print("OIMcurrencyButton❗️❗️", availableVal, value)
+ }
+ }
let disabled = !canEdit || isMorphing || unavailable // cannot edit morphing funds
EmptyView().modifier(OIMmod(name: imgName(outValue ?? value),
flippedName: imgName(flippedVal),
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyDrawer.swift b/TalerWallet1/Views/OIM/OIMcurrencyDrawer.swift
@@ -30,12 +30,14 @@ struct OIMcurrencyDrawer: View {
HStack(alignment: .bottom, spacing: 10) {
ForEach(currency.bankNotes, id: \.self) { value in
let sourceID = -Int(value)
- let fund = OIMfund(id: sourceID, state: .idle, value: value)
+ let fund = OIMfund(id: sourceID, value: value,
+ currencyIndex: cash.currencyIndex)
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: currency,
availableVal: availableVal,
canEdit: canEdit,
+ isDrawer: true,
pct: 0.9, // hover a bit above the desk
action: { tappedVal = value }
)
@@ -44,12 +46,14 @@ struct OIMcurrencyDrawer: View {
}
ForEach(currency.bankCoins, id: \.self) { value in
let sourceID = -Int(value)
- let fund = OIMfund(id: sourceID, state: .idle, value: value)
+ let fund = OIMfund(id: sourceID, value: value,
+ currencyIndex: cash.currencyIndex)
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: currency,
availableVal: availableVal,
canEdit: canEdit,
+ isDrawer: true,
pct: 0.9,
action: { tappedVal = value }
)
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -56,7 +56,7 @@ extension LayoutSubview {
// renders a stack of funds (banknotes, coins)
@available(iOS 16.4, *)
struct OIMlayoutView: View {
- private let symLog = SymLogV(0)
+ private let symLog = SymLogV()
// let logger = Logger(subsystem: "net.taler.gnu", category: "OIMlayoutView")
let stack: CallStack
let cash: OIMcash
@@ -67,25 +67,40 @@ struct OIMlayoutView: View {
@EnvironmentObject private var wrapper: NamespaceWrapper
@State private var checkStacks: UInt64 = 0
- func startFlying(fundID: Int) {
+ func endFlying(_ index: Int, after delay: Double) {
+ var fund = funds[index]
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ symLog.log("*** end flying:\(fund.value) \(fund.id)")
+ withAnimation(.move1) {
+ fund.state = .idle // move onto stack
+ cash.updateFund(fund)
+ }
+ if checkStacks > fund.value || checkStacks == 0 {
+ symLog.log("*** checkStacks:\(fund.value)")
+ checkStacks = fund.value
+ }
+ }
+ }
+
+ func startFlying(fundID: Int, fromChest: Bool = false) {
if let index = funds.firstIndex(where: { $0.id == fundID }) {
var fund = funds[index]
symLog.log("*** start flying:\(fund.value) \(fundID)")
+ if fromChest && fund.delay > 0 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + fund.delay) {
+ withAnimation(.move1) {
+ fund.state = .curve
+ cash.updateFund(fund)
+ }
+ endFlying(index, after: cash.flyDelay / 3)
+ }
+ } else {
+ endFlying(index, after: cash.flyDelay)
+ }
withAnimation(.move1) {
- fund.state = .isFlying // switch off matching
+ fund.state = fromChest ? .reveal : .isFlying // switch off matching
cash.updateFund(fund)
}
- DispatchQueue.main.asyncAfter(deadline: .now() + cash.delay) {
- symLog.log("*** end flying:\(fund.value) \(fundID)")
- withAnimation(.move1) {
- fund.state = .idle // move onto stack
- cash.updateFund(fund)
- }
- if checkStacks > fund.value || checkStacks == 0 {
- symLog.log("*** checkStacks:\(fund.value)")
- checkStacks = fund.value
- }
- }
}
}
@@ -97,7 +112,7 @@ struct OIMlayoutView: View {
fund.state = .returning
cash.updateFund(fund)
}
- DispatchQueue.main.asyncAfter(deadline: .now() + cash.delay) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + cash.flyDelay) {
symLog.log("*** remove:\(fund.value) \(fundID)")
withAnimation(.move1) {
cash.removeCash(id: fundID, value: fund.value)
@@ -112,13 +127,14 @@ struct OIMlayoutView: View {
let value = fund.value
let fundID = fund.id
let fundState = fund.state
- let shouldFly = fundState == .shouldFly
+ let shouldFly = fundState.shouldFly
// let willMutate = fundState == .arriving || fundState == .mutating
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: cash.currency,
availableVal: value,
canEdit: canEdit,
+ isDrawer: false,
pct: shouldFly ? 0.0 : 1.0
) { // remove on button press
if amountVal >= fund.value {
@@ -139,7 +155,8 @@ struct OIMlayoutView: View {
.onAppear {
if shouldFly {
startFlying(fundID: fundID)
- } else {
+ } else if fundState.chestOpening {
+ startFlying(fundID: fundID, fromChest: true)
// print(" ->OIMlayout ForEach fund.onAppear ignore \(value), \(fundID)")
}
}