commit 252a53c528c5f717ab694d23ab4c731ff7805d97
parent a18272cce61d595334685048e0f3d3ad197ffae0
Author: Marc Stibane <marc@taler.net>
Date: Sat, 19 Apr 2025 00:26:26 +0200
finetuning
Diffstat:
3 files changed, 109 insertions(+), 98 deletions(-)
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -22,8 +22,18 @@ enum FundState: Int {
case position // position to move to
case moving // move to position
case hiding // hide at position
- case morphing // start -180
- case morphed // flip 180 + change value
+ case morphing // flip
+ case arriving // keep flipped image, move to final position
+ case mutating // flip back with disbled transition
+
+ var flipping: Bool { self == .flipping }
+ var shouldFly: Bool { self == .shouldFly }
+ var isFlying: Bool { self == .isFlying }
+ var position: Bool { self == .position }
+ var morphing: Bool { self == .morphing }
+ var moving: Bool { self == .moving }
+ var hiding: Bool { self == .hiding }
+ var mutating: Bool { self == .mutating }
}
/// data structure for a cash item on the table
@@ -31,22 +41,17 @@ 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 morphTicker: Int? // `semaphore´ to reserve a fund - don't morph this twice
- var flippedVal: UInt64? // the value to morph into
+ 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 ? -Int(value) // match sourceID
- : id)
- }
- var shouldFly: Bool {
- state == .shouldFly
- }
- var isFlying: Bool {
- state == .isFlying
- }
- var isFlipping: Bool {
- state == .flipping
+ String(state.shouldFly ? -Int(value) // match sourceID
+ : id)
}
+ var shouldFly: Bool { state.shouldFly }
+ var isFlying: Bool { state.isFlying }
+ var isFlipping: Bool { state.flipping }
}
public typealias OIMfunds = [OIMfund]
@@ -152,11 +157,11 @@ final class OIMcash: ObservableObject, Sendable {
added -= 1
symLog.log(">>taking this anchor \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
withAnimation(.basic1) {
- morphFund.flippedVal = inValue // layout.sortByValue will take this instead of value
- morphFund.state = .position // to compute the position where the morph happens
+ morphFund.outValue = outValue // save old image
+ morphFund.flippedVal = inValue // layout.sortByValue will take this instead of value
+ morphFund.state = .position // to compute the position where the morph happens
updateFund(morphFund)
}
-
withAnimation(.easeInOutDelay2) {
symLog.log(">>moving outgoing to \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
for var delFund in toDelete { // move the rest of the leaving funds
@@ -165,7 +170,7 @@ final class OIMcash: ObservableObject, Sendable {
}
}
- /// Stage 2: After arriving at the position, without animation hide all outValues (except `morphing´)
+ /// Stage 2: After arriving at the position, without animation hide all outValues (except our anchor)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
for var delFund in toDelete {
self.symLog.log(">>hide \(delFund.id)")
@@ -173,38 +178,25 @@ final class OIMcash: ObservableObject, Sendable {
self.updateFund(delFund) // TODO: check if stack remains!!!
}
- /// Stage 3: prepare flipping
+ /// Stage 3: flip animated
morphFund.state = .morphing
- self.symLog.log(">>prepare \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
- self.updateFund(morphFund)
- /// flip animated
- morphFund.state = .morphed
self.symLog.log(">>flip \(morphFund.id), \(morphFund.value) \(morphFund.state.rawValue)")
-// withAnimation(.easeIn1) { - not needed, the button animates itself when flipping
- self.updateFund(morphFund)
-// }
- self.symLog.log(">>flipped \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
+ self.updateFund(morphFund) // withAnimation not needed, it animates itself when flipping
+ self.symLog.log(">>flipped \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
- /// Stage 4: move inValue to final position
+ /// Stage 4: move inValues to final position, delete outValues from funds array
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
- /// transmute the fund
- morphFund.value = inValue
- morphFund.flippedVal = nil
+ morphFund.state = .arriving
+ morphFund.value = inValue // stack position
morphFund.morphTicker = nil // release `semaphore´
- self.symLog.log(">>transmute \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
- self.updateFund(morphFund)
-
-
-
- morphFund.state = .idle
self.symLog.log(">>move to final position \(morphFund.id) \(morphFund.state.rawValue)")
withAnimation(.move1) {
self.updateFund(morphFund)
while added > 0 {
var fund = toDelete.removeFirst()
- fund.value = inValue // transmute to inValue
+ fund.value = inValue // transmute to inValue (without flip)
fund.morphTicker = nil // release `semaphore´
- fund.state = .idle
+ fund.state = .idle // move to their positions
self.updateFund(fund)
added -= 1
}
@@ -214,6 +206,25 @@ final class OIMcash: ObservableObject, Sendable {
self.removeCash(id: delFund.id, value: delFund.value)
}
}
+
+ /// Stage 5: transmute morphFund
+ DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
+ morphFund.state = .mutating
+// morphFund.state = .idle
+ morphFund.outValue = nil
+ morphFund.flippedVal = nil
+ self.symLog.log(">>transmute \(morphFund.id) \(morphFund.value) \(morphFund.state.rawValue)")
+ self.updateFund(morphFund)
+
+ /// Stage 6: back to idle
+ DispatchQueue.main.async {
+ withAnimation(.move1) {
+ morphFund.state = .idle
+ self.updateFund(morphFund)
+ }
+ }
+ }
+
}
} // asyncAfter delay
return counter == 0
@@ -287,7 +298,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 == .flipping }) {
+ } else if let index = funds.lastIndex(where: { $0.value == value && $0.state.flipping }) {
funds.remove(at: index)
} else if let index = funds.firstIndex(where: { $0.value == value }) {
funds.remove(at: index)
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyButton.swift b/TalerWallet1/Views/OIM/OIMcurrencyButton.swift
@@ -44,13 +44,14 @@ struct OIMcurrencyImage {
fileprivate
struct OIMmod: AnimatableModifier {
- let value: UInt64
let name: String?
let flippedName: String?
- let availableVal: UInt64
- let canEdit: Bool
- let invisible: Bool
+ let flipTime: TimeInterval
let isFlipped: Bool
+ let flipBack: Bool // if true, then don't animate flipping back
+ let disabled: Bool
+ let unavailable: Bool
+ let invisible: Bool
var pct: CGFloat
let action: () -> Void
@@ -60,7 +61,6 @@ struct OIMmod: AnimatableModifier {
}
func body(content: Content) -> some View {
- let shadow = (3 - 8) * pct + 8
let currencyImage = OIMcurrencyImage(name)
let oWidth = currencyImage.oWidth / 4
let oHeight = currencyImage.oHeight / 4
@@ -74,45 +74,40 @@ struct OIMmod: AnimatableModifier {
let backImage = Group {
if let flippedName {
let flippedImage = OIMcurrencyImage(flippedName)
-// let _ = print(" OIMmod", isFlipped, value, flippedName)
flippedImage.image
.resizable()
.scaledToFit()
} else {
-// if isFlipped {
-// let _ = print(" OIMmod no image", isFlipped, value, flippedName)
-// }
EmptyView()
}
}
.rotation3DEffect(.degrees(isFlipped ? 0 : -90),
axis: (x: 0.0, y: 1.0, z: 0.0))
+
+ let linTime: Animation = .linear(duration:flipTime)
+ let frontAnimation = isFlipped ? linTime
+ : linTime.delay(flipTime)
+ let backAnimation = isFlipped ? linTime.delay(flipTime)
+ : linTime
let animatedImage = ZStack {
valueImage
- .animation(isFlipped ? .linear : .linear.delay(0.35), value: isFlipped)
+ .animation(flipBack ? nil : frontAnimation, value: isFlipped)
backImage
- .animation(isFlipped ? .linear.delay(0.35) : .linear, value: isFlipped)
+ .animation(flipBack ? nil : backAnimation, value: isFlipped)
}
- return Group {
- if invisible {
- image.opacity(0)
- } else if !canEdit || availableVal < value {
- animatedImage
- .opacity(canEdit ? 0.5 : 1.0)
- } else {
- Button(action: action) { // TODO: disable after tapped
- animatedImage
+ let shadow = (3 - 8) * pct + 8
+ return Button(action: action) {
+ animatedImage.opacity(invisible ? 0.001
+ : unavailable ? 0.5
+ : 1.0)
}
.buttonStyle(OIMbuttonStyle())
- }
- } // Group
-// .frame(width: (isFlipped != 0 ? 0.7 * oWidth : oWidth),
-// height: (isFlipped != 0 ? 0.7 * oHeight : oHeight))
- .frame(width: oWidth, height: oHeight)
- .shadow(radius: shadow)
- .accessibilityLabel(Text("\(value)", comment: "VoiceOver")) // TODO: currency name
+ .disabled(disabled)
+ .frame(width: oWidth, height: oHeight)
+ .shadow(radius: shadow)
}
+
}
// MARK: -
/// renders 1 denomination from a currency
@@ -125,8 +120,6 @@ struct OIMcurrencyButton: View {
var pct: CGFloat
let action: () -> Void
- @EnvironmentObject private var cash: OIMcash
-
func imgName(_ value: UInt64?) -> String? {
if let value {
return value > currency.bankCoins[0] ? currency.noteName(value)
@@ -136,33 +129,37 @@ struct OIMcurrencyButton: View {
}
var body: some View {
- let currency = cash.currency
let value = fund.value
+ let outValue = fund.outValue
+ let state = fund.state
let isMorphing = fund.morphTicker != nil
let flippedVal = fund.flippedVal
// Use EmptyView, because the modifier actually ignores
// the value passed to its body() function.
- let isFlipped: Bool = switch fund.state {
- case .morphed: true
+ let isFlipped: Bool = switch state {
case .flipping: true
+ case .morphing: true
+ case .arriving: true
default: false
}
-// if fund.id == 6 {
-// let _ = print("Fund", fund.id, value, fund.state, flippedVal)
+// if fund.id >= 0 {
+// let _ = print("***Fund", fund.id, value, state, flippedVal)
// }
- EmptyView().modifier(OIMmod(value: value,
- name: imgName(value),
- flippedName: imgName(flippedVal),
- availableVal: availableVal,
- canEdit: canEdit && !isMorphing, // cannot edit morphing funds
- invisible: fund.state == .hiding,
- isFlipped: isFlipped && flippedVal != nil,
- pct: pct,
- action: action))
+ let mutating = state.mutating
+ let flipTime = mutating ? 0.0001 // flip back quickly from morphed
+ : fastAnimations ? 0.3
+ : 0.5
+ // checkDisabled = true // TODO: don't run twice
+ EmptyView().modifier(OIMmod(name: imgName(outValue ?? value),
+ flippedName: imgName(flippedVal),
+ flipTime: flipTime,
+ isFlipped: isFlipped,
+ flipBack: mutating,
+ disabled: !canEdit || isMorphing, // cannot edit morphing funds
+ unavailable: availableVal < value,
+ invisible: state.hiding,
+ pct: pct,
+ action: action))
+ .accessibilityLabel(Text("\(value)", comment: "VoiceOver")) // TODO: currency name
}
}
-
-// MARK: -
-//#Preview {
-// OIMcurrencyView()
-//}
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -113,6 +113,7 @@ struct OIMlayoutView: View {
let fundID = fund.id
let fundState = fund.state
let shouldFly = fundState == .shouldFly
+// let willMutate = fundState == .arriving || fundState == .mutating
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: cash.currency,
@@ -178,7 +179,8 @@ struct OIMlayout: Layout {
let isMorphing1 = state1 == .position
|| state1 == .morphing
- || state1 == .morphed
+ || state1 == .arriving
+ || state1 == .mutating
if isMorphing1 {
if flipV1 > 0 {
let firstIsBigger = value0 > flipV1
@@ -192,7 +194,8 @@ struct OIMlayout: Layout {
let isMorphing0 = state0 == .position
|| state0 == .morphing
- || state0 == .morphed
+ || state0 == .arriving
+ || state0 == .mutating
if isMorphing0 {
if flipV0 > 0 {
let firstIsBigger = flipV0 > value1
@@ -225,15 +228,15 @@ struct OIMlayout: Layout {
let value = subview.oimValue
let state = subview.oimFundState
let id = subview.oimID
+// let flippedVal = subview.oimFlippedVal ?? 0
var ignore = 0
- if state == .shouldFly || state == .isFlying // flying can not go on the stack
- || state == .position // nor can the position where we morph...
- || state == .morphing // ...or the morphing fund...
- || state == .morphed { // ...or morphed fund
+ if state.shouldFly || state.isFlying // flying can not go on the stack
+ || state.position // nor can the position where we morph...
+ || state.morphing { // ...or the morphing fund
// we always morph out from the end, thus there will be no more idling funds with the same value
lastValue = 0 // let the next subview...
stackIndex = 0 // ...start a new stack
- } else if state == .moving || state == .hiding {
+ } else if state.moving || state.hiding {
ignore = -1
} else if lastValue != value { // different value?
lastValue = value // save this value for the next subview
@@ -353,12 +356,12 @@ struct OIMlayout: Layout {
let subview = sorted[idx]
let id = subview.oimID
let state = subview.oimFundState
- if state == .position || state == .morphing || state == .morphed {
+ if state.position || state.morphing {
// if morphPt != pt { print("set morphPt \(morphPt) to ク\(subview.oimValue),\(subview.oimID) at", pt) }
morphPt = pt
}
subview.place(at: pt, anchor: .topLeading, proposal: proposal)
- if state == .moving || state == .hiding {
+ if state.moving || state.hiding {
if morphPt != CGPoint.zero {
// print("morphing ク\(subview.oimValue) \(subview.oimID) \(state) from \(pt) to", morphPt)
subview.place(at: morphPt, anchor: .topLeading, proposal: proposal)