commit a18272cce61d595334685048e0f3d3ad197ffae0
parent 0c150692efef040a8a8e8e8d0a1d768de9b8db99
Author: Marc Stibane <marc@taler.net>
Date: Fri, 18 Apr 2025 18:00:07 +0200
animations
Diffstat:
6 files changed, 382 insertions(+), 218 deletions(-)
diff --git a/TalerWallet1/Views/OIM/OIM15Views.swift b/TalerWallet1/Views/OIM/OIM15Views.swift
@@ -39,7 +39,7 @@ struct OIMnoteStackV: View {
let xOffset = CGFloat(10 * index)
let yOffset = CGFloat(20 * index)
let _ = print("targetID \(targetID), flying \(flying)")
- let fund = OIMfund(id: Int(value), value: value, state: .idle)
+ let fund = OIMfund(id: Int(value), state: .idle, value: value, flippedVal: nil)
OIMcurrencyButton(stack: stack.push(),
// value: value,
fund: fund,
@@ -53,7 +53,7 @@ struct OIMnoteStackV: View {
.matchedGeometryEffect(id: targetID, in: wrapper.namespace, isSource: false)
.onAppear {
print("start flying \(targetID)")
- withAnimation(.fly1) {
+ withAnimation(.move1) {
flying = value // start flying
}
}
@@ -93,7 +93,7 @@ struct OIMcoinStackV: View {
let yOffset = offset * CGFloat(index)
let xOffset = yOffset / 2
let _ = print("targetID \(targetID), flying \(flying)")
- let fund = OIMfund(id: Int(value), value: value, state: .idle) // TODO: Flip coin
+ let fund = OIMfund(id: Int(value), state: .idle, value: value, flippedVal: nil) // TODO: Flip coin
OIMcurrencyButton(stack: stack.push(),
// value: value,
fund: fund,
@@ -107,7 +107,7 @@ struct OIMcoinStackV: View {
.matchedGeometryEffect(id: targetID, in: wrapper.namespace, isSource: false)
.onAppear {
print("start flying \(targetID)")
- withAnimation(.fly1) {
+ withAnimation(.move1) {
flying = value // start flying
}
}
@@ -147,7 +147,7 @@ struct OIMnotesView1: View {
flying: $flying,
canEdit: canEdit
) {
- withAnimation(.fly1) {
+ withAnimation(.easeOut1) {
amountVal -= value // remove on button press
} } }
} // ForEach
@@ -181,7 +181,7 @@ struct OIMcoinsView1: View {
flying: $flying,
canEdit: canEdit
) {
- withAnimation(Animation.easeIn1) {
+ withAnimation(.easeIn1) {
amountVal -= value
} } }
} // ForEach
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -7,34 +7,54 @@
*/
import SwiftUI
import taler_swift
+import os.log
+import SymLog
let MAXSTACK = 4
enum FundState: Int {
case idle
+ case flipping // user tapped to remove
+
case shouldFly // + button tapped
case isFlying // after onAppear
- case flipping // user tapped to remove
- case morphingOut // too much in stack
- case morphingIn // replacement
- case isMorphing //
+
+ 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
}
/// 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
- let value: UInt64
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 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
+ }
+
}
public typealias OIMfunds = [OIMfund]
// MARK: -
-class OIMcash: ObservableObject {
+final class OIMcash: ObservableObject, Sendable {
+ private let symLog = SymLogV(0)
+ let logger = Logger(subsystem: "net.taler.gnu", category: "OIMcash")
var ticker = 0 // increment each time a fund is added (or melted from others)
var currency: OIMcurrency
@Published var funds: OIMfunds = []
@@ -52,26 +72,35 @@ class OIMcash: ObservableObject {
}
}
+ var delay: Double {
+#if DEBUG
+ if debugAnimations { return 5.0 }
+// return fastAnimations ? 5 : 1.0
+#endif
+ return fastAnimations ? 0.4 : 0.8
+ }
+
+
func sortByValue() -> OIMfunds {
- // sorts ASCENDING - we check small values first
- funds.sorted(by: { ($0.value < $1.value) ||
- ( ($0.value == $1.value) && ($0.id < $1.id) ) }) // but keep the order of the IDs
+ /// sorts ASCENDING but keeps the order of the IDs for identical values
+ funds.sorted(by: {
+ ($0.value < $1.value) ||
+ (($0.value == $1.value) && ($0.id > $1.id))
+ })
}
func checkStacks(first firstCheck: UInt64, _ max: Int = MAXSTACK) -> UInt64? {
+ let sorted = sortByValue()
/// same algorithm as OIMlayout.computeSpaces
/// result is 0 if all stacks have 4 or less items
- /// returns highest value with more than 4 items
-
- let sorted = sortByValue()
- let count = sorted.count
+ /// returns lowest fund.value with more than 4 items
var stackIndex = 0
var lastValue: UInt64 = 0
- for (index, fund) in sorted.enumerated() {
+ for fund in sorted {
let value = fund.value
let state = fund.state
- if state == .shouldFly || state == .isFlying { // flying can not go on the stack
+ if fund.shouldFly || fund.isFlying { // 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?
@@ -85,54 +114,113 @@ class OIMcash: ObservableObject {
}
return nil
}
-
- func convert(_ nr: UInt64, of stackValue: UInt64, to biggerDenom: UInt64) -> Bool {
+//MARK: -
+ func morph(_ nr: UInt64, of outValue: UInt64, to inValue: UInt64) -> Bool {
+ let morphTicker = ticker // capture ticker at start of morphing
+ symLog.log(" Start morphing #\(morphTicker)")
let sorted = sortByValue()
- var counter = nr
var toDelete: OIMfunds = []
- var amount: UInt64 = 0
- for var fund in sorted {
- if fund.value == stackValue && counter > 0 {
- withAnimation(.basic1) {
- fund.state = .morphingOut
- updateFund(fund)
- }
+
+ var amount: UInt64 = 0 // ensure sum(morphedOut) is sum(morphedIn)
+ var counter = nr // counts outValues to be morphed
+ var added = 0 // counts inValues to be morphed
+ var first = true
+ for var fund in sorted { // remove newest funds first
+ if fund.value == outValue && fund.morphTicker == nil && counter > 0 {
+ fund.morphTicker = first ? morphTicker : -morphTicker
+ first = false
+ updateFund(fund)
toDelete.append(fund)
counter -= 1
- amount += stackValue
+ amount += outValue
}
}
- DispatchQueue.main.async {
- var added: OIMfunds = []
- withAnimation(.basic1) {
- while amount >= biggerDenom {
- let fund = self.addCash(value: biggerDenom, .morphingIn)
- print(">>morphing to", fund.value, fund.id)
- added.append(fund)
- amount -= biggerDenom
- }
- }
- withAnimation(.remove1) {
- for fund in toDelete {
- self.removeCash(id: fund.id, value: fund.value)
- }
+ if counter > 0 {
+ logger.warning(" ❗️Yikes: didn't find \(nr) funds to morph, missing \(counter)")
+ }
+
+ while amount >= inValue {
+ amount -= inValue
+ added += 1 // TODO: If we added more than 1, then we must morph more
+ }
+ if amount > 0 {
+ logger.warning(" ❗️Yikes: morph leftover:\(amount)")
+ }
+
+ /// Stage 1: the fund with the highest id just got added - that is the anchor position for the morph
+ var morphFund = toDelete.removeFirst()
+ 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
+ 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
+ delFund.state = .moving // to the morphing position
+ updateFund(delFund)
}
- withAnimation(.fly1) {
- for var fund in added {
- print("finish morphing", fund.id)
- fund.state = .idle
- self.updateFund(fund)
- }
+ }
+
+ /// Stage 2: After arriving at the position, without animation hide all outValues (except `morphing´)
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
+ for var delFund in toDelete {
+ self.symLog.log(">>hide \(delFund.id)")
+ delFund.state = .hiding // make invisible
+ self.updateFund(delFund) // TODO: check if stack remains!!!
}
- if amount > 0 {
- print(" ❗️Yikes: convert failed, leftover:", amount)
+ /// Stage 3: prepare flipping
+ 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)")
+
+ /// Stage 4: move inValue to final position
+ DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
+ /// transmute the fund
+ morphFund.value = inValue
+ morphFund.flippedVal = nil
+ 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.morphTicker = nil // release `semaphore´
+ fund.state = .idle
+ self.updateFund(fund)
+ added -= 1
+ }
+ // remaining outValues were already invisible, but we need to animate the stack they came from
+ for var delFund in toDelete {
+ self.symLog.log(">>remove \(delFund.id)")
+ self.removeCash(id: delFund.id, value: delFund.value)
+ }
+ }
}
- }
+ } // asyncAfter delay
return counter == 0
}
func compactStacks(_ value: UInt64) -> Bool {
+ symLog.log(" Start compactStacks \(value)")
let denominations = currency.bankNotes + currency.bankCoins
if let index = denominations.firstIndex(where: { $0 == value }) {
if index > 0 { // does a bigger denomination exist?
@@ -140,7 +228,7 @@ class OIMcash: ObservableObject {
let nextIndex = index-1
let nextValue = denominations[nextIndex] // let next = next_bigger_denomination
if nextValue == value5x {
- return convert(5, of: value, to: nextValue)
+ return morph(5, of: value, to: nextValue)
}
// since we want to "convert" adjacent denominations (some smaller will become 1 larger fund),
// we cannot use the whole 5 smaller funds (4 in stack, 1 just added) if next is not 5 times bigger
@@ -152,7 +240,7 @@ class OIMcash: ObservableObject {
let secondValue = denominations[secondIdx]
if secondValue <= value5x && secondValue.isMultiple(of: value) {
let nrToDelete = secondValue / value
- return convert(nrToDelete, of: value, to: secondValue)
+ return morph(nrToDelete, of: value, to: secondValue)
}
}
}
@@ -160,7 +248,7 @@ class OIMcash: ObservableObject {
// or there are already some funds of next on the table
if nextValue.isMultiple(of: value) {
let nrToDelete = nextValue / value
- return convert(nrToDelete, of: value, to: nextValue)
+ return morph(nrToDelete, of: value, to: nextValue)
}
// Now this is tricky - there are already some funds of next on the table, but next is not a multiple of value
// But maybe 2 of next are a multiple - e.g. value=10, next=25
@@ -168,7 +256,7 @@ class OIMcash: ObservableObject {
if nextValue2x.isMultiple(of: value) {
let nrToDelete = nextValue * 2 / value
if nrToDelete <= 5 {
- return convert(nrToDelete, of: value, to: nextValue) // 5*10 = 2*25
+ return morph(nrToDelete, of: value, to: nextValue) // 5*10 = 2*25
}
}
}
@@ -188,10 +276,10 @@ class OIMcash: ObservableObject {
return funds.filter { $0.value <= firstCoinVal }
}
- func addCash(value: UInt64, _ newState: FundState = .shouldFly) -> OIMfund {
- let fund = OIMfund(id: ticker, value: value, state: newState)
+ func addCash(value: UInt64, _ newState: FundState = .shouldFly, _ flippedVal: UInt64? = nil) -> OIMfund {
+ let fund = OIMfund(id: ticker, state: newState, value: value, flippedVal: flippedVal)
ticker += 1
- print(">>adding", value, fund.id)
+ symLog.log(">>adding \(value) \(fund.id)")
funds.append(fund)
return fund
}
@@ -231,7 +319,7 @@ class OIMcash: ObservableObject {
var count = shownNotes.count
while count > wanted {
let note = shownNotes[0]
- print("update: remove \(note.value)")
+ symLog.log("update: remove \(note.value)")
if let index = array.firstIndex(of: note) {
array.remove(at: index)
changed = true
@@ -240,9 +328,9 @@ class OIMcash: ObservableObject {
count -= 1
}
while count < wanted {
- let note = OIMfund(id: ticker, value: value, state: .idle)
+ let note = OIMfund(id: ticker, state: .idle, value: value, flippedVal: nil)
ticker += 1
- print("update: add \(note.value)")
+ symLog.log("update: add \(note.value)")
array.append(note)
changed = true
count += 1
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyButton.swift b/TalerWallet1/Views/OIM/OIMcurrencyButton.swift
@@ -46,8 +46,10 @@ fileprivate
struct OIMmod: AnimatableModifier {
let value: UInt64
let name: String?
+ let flippedName: String?
let availableVal: UInt64
let canEdit: Bool
+ let invisible: Bool
let isFlipped: Bool
var pct: CGFloat
let action: () -> Void
@@ -60,27 +62,55 @@ struct OIMmod: AnimatableModifier {
func body(content: Content) -> some View {
let shadow = (3 - 8) * pct + 8
let currencyImage = OIMcurrencyImage(name)
- let image = currencyImage.image
- .resizable()
- .scaledToFit()
let oWidth = currencyImage.oWidth / 4
let oHeight = currencyImage.oHeight / 4
+ let image = currencyImage.image
+ .resizable()
+ .scaledToFit()
+
+ let valueImage = image
+ .rotation3DEffect(.degrees(isFlipped ? 90 : 0),
+ axis: (x: 0.0, y: 1.0, z: 0.0))
+ 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 animatedImage = ZStack {
+ valueImage
+ .animation(isFlipped ? .linear : .linear.delay(0.35), value: isFlipped)
+ backImage
+ .animation(isFlipped ? .linear.delay(0.35) : .linear, value: isFlipped)
+ }
+
return Group {
- if canEdit && availableVal >= value {
- Button(action: action) {
- image
+ 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
}
.buttonStyle(OIMbuttonStyle())
- } else {
- image
- .opacity(canEdit ? 0.5 : 1.0)
}
- }
- .frame(width: (isFlipped ? 0.7 * oWidth : oWidth),
- height: (isFlipped ? 0.7 * oHeight : oHeight))
+ } // Group
+// .frame(width: (isFlipped != 0 ? 0.7 * oWidth : oWidth),
+// height: (isFlipped != 0 ? 0.7 * oHeight : oHeight))
+ .frame(width: oWidth, height: oHeight)
.shadow(radius: shadow)
- .rotation3DEffect(.degrees(isFlipped ? 135 : 0), axis: (x: 0, y: 1, z: 0))
- .animation(.basic1, value: isFlipped)
.accessibilityLabel(Text("\(value)", comment: "VoiceOver")) // TODO: currency name
}
}
@@ -97,18 +127,36 @@ struct OIMcurrencyButton: View {
@EnvironmentObject private var cash: OIMcash
+ func imgName(_ value: UInt64?) -> String? {
+ if let value {
+ return value > currency.bankCoins[0] ? currency.noteName(value)
+ : currency.coinName(value)
+ }
+ return nil
+ }
+
var body: some View {
let currency = cash.currency
let value = fund.value
- let name = value > currency.bankCoins[0] ? currency.noteName(value)
- : currency.coinName(value)
+ 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
+ case .flipping: true
+ default: false
+ }
+// if fund.id == 6 {
+// let _ = print("Fund", fund.id, value, fund.state, flippedVal)
+// }
EmptyView().modifier(OIMmod(value: value,
- name: name,
+ name: imgName(value),
+ flippedName: imgName(flippedVal),
availableVal: availableVal,
- canEdit: canEdit,
- isFlipped: fund.state == .flipping,
+ canEdit: canEdit && !isMorphing, // cannot edit morphing funds
+ invisible: fund.state == .hiding,
+ isFlipped: isFlipped && flippedVal != nil,
pct: pct,
action: action))
}
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift b/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift
@@ -22,7 +22,7 @@ struct OIMcurrencyScroller: View {
ScrollView(.horizontal) {
HStack(alignment: .bottom, spacing: 10) {
ForEach(currency.bankNotes, id: \.self) { value in
- let fund = OIMfund(id: -Int(value), value: value, state: .idle)
+ let fund = OIMfund(id: -Int(value), state: .idle, value: value, flippedVal: nil)
let sourceID = -Int(value)
OIMcurrencyButton(stack: stack.push(),
fund: fund,
@@ -35,7 +35,7 @@ struct OIMcurrencyScroller: View {
.matchedGeometryEffect(id: String(sourceID), in: wrapper.namespace, isSource: true)
}
ForEach(currency.bankCoins, id: \.self) { value in
- let fund = OIMfund(id: -Int(value), value: value, state: .idle)
+ let fund = OIMfund(id: -Int(value), state: .idle, value: value, flippedVal: nil)
let sourceID = -Int(value)
OIMcurrencyButton(stack: stack.push(),
fund: fund,
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -7,13 +7,20 @@
*/
import SwiftUI
import SymLog
+import os.log
struct OIMid: LayoutValueKey {
static let defaultValue = 0
}
+struct OIMmorph: LayoutValueKey {
+ static let defaultValue = 0
+}
struct OIMvalue: LayoutValueKey {
static let defaultValue: UInt64 = 0
}
+struct OIMflippedval: LayoutValueKey {
+ static let defaultValue: UInt64 = 0
+}
struct OIMfundState: LayoutValueKey {
static let defaultValue: FundState = .idle
}
@@ -23,9 +30,15 @@ extension View {
func oimID(_ value: Int) -> some View {
self.layoutValue(key: OIMid.self, value: value)
}
+ func oimMorph(_ value: Int) -> some View {
+ self.layoutValue(key: OIMmorph.self, value: value)
+ }
func oimValue(_ value: UInt64) -> some View {
self.layoutValue(key: OIMvalue.self, value: value)
}
+ func oimFlippedVal(_ value: UInt64) -> some View {
+ self.layoutValue(key: OIMflippedval.self, value: value)
+ }
func oimFundState(_ value: FundState) -> some View {
self.layoutValue(key: OIMfundState.self, value: value)
}
@@ -35,14 +48,16 @@ extension View {
extension LayoutSubview {
var oimID: Int { self[OIMid.self] }
var oimValue: UInt64 { self[OIMvalue.self] }
+ var oimFlippedVal: UInt64 { self[OIMflippedval.self] }
var oimFundState: FundState { self[OIMfundState.self] }
}
// MARK: -
-// renders a stack of (identical) banknotes with offset 10,20
+// renders a stack of funds (banknotes, coins)
@available(iOS 16.4, *)
struct OIMlayoutView: View {
private let symLog = SymLogV(0)
+// let logger = Logger(subsystem: "net.taler.gnu", category: "OIMlayoutView")
let stack: CallStack
let funds: OIMfunds
@Binding var amountVal: UInt64
@@ -52,98 +67,90 @@ struct OIMlayoutView: View {
@EnvironmentObject private var wrapper: NamespaceWrapper
@State private var checkStacks: UInt64 = 0
+ func startFlying(fundID: Int) {
+ if let index = funds.firstIndex(where: { $0.id == fundID }) {
+ var fund = funds[index]
+ symLog.log("*** start flying:\(fund.value) \(fundID)")
+ withAnimation(.move1) {
+ fund.state = .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
+ }
+ }
+ }
+ }
+
+ func flipRemove(fundID: Int) {
+ if let index = funds.firstIndex(where: { $0.id == fundID }) {
+ var fund = funds[index]
+ symLog.log("*** start flipping:\(fund.value) \(fundID)")
+ withAnimation(.basic1) {
+ fund.state = .flipping
+ cash.updateFund(fund)
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + cash.delay) {
+ symLog.log("*** remove:\(fund.value) \(fundID)")
+ withAnimation(.move1) {
+ cash.removeCash(id: fundID, value: fund.value)
+ }
+ }
+ }
+ }
+
var body: some View {
OIMlayout {
- ForEach(funds){ item in
- var fund = item
-
+ ForEach(funds) { fund in
let value = fund.value
+ let fundID = fund.id
let fundState = fund.state
let shouldFly = fundState == .shouldFly
- let isFlipped = fundState == .flipping
- let shouldMorph = fundState == .morphingIn
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: cash.currency,
availableVal: value,
- canEdit: canEdit && !isFlipped,
+ canEdit: canEdit,
pct: shouldFly ? 0.0 : 1.0
) { // remove on button press
if amountVal >= fund.value {
amountVal -= fund.value
} else {
- symLog.log(" ❗️Yikes - trying to make amount negative")
+ symLog.log(" ❗️Yikes - trying to subtract \(fund.value) from amount \(amountVal)")
amountVal = 0
}
- withAnimation(.basic1) {
- fund.state = .flipping
- cash.updateFund(fund)
- }
- DispatchQueue.main.async {
- withAnimation(.remove1) {
- symLog.log(" OIMlayoutView remove \(value)")
- cash.removeCash(id: fund.id, value: fund.value)
- }
- }
+ flipRemove(fundID: fundID)
}
- .zIndex(Double(fund.id))
- .id(fund.id)
+ .zIndex(Double(fundID))
+ .id(fundID)
.oimValue(value)
- .oimID(fund.id)
+ .oimFlippedVal(fund.flippedVal ?? 0)
+ .oimID(fundID)
.oimFundState(fundState)
- .matchedGeometryEffect(id: fund.targetID, in: wrapper.namespace, isSource: false) // !isFlying
+ .matchedGeometryEffect(id: fund.targetID, in: wrapper.namespace, isSource: false)
.onAppear {
if shouldFly {
-// print(" ->OIMlayoutView.onAppear stop matching \(value), \(fund.id)")
- withAnimation(.fly1) {
- print("*** start flying:", value, fund.id)
- fund.state = .isFlying // switch off matching
- cash.updateFund(fund)
- }
- DispatchQueue.main.asyncAfter(deadline: .now() + (fastAnimations ? 0.6 : 1.0)) {
- withAnimation(.fly1) {
- print("*** end flying:", value, fund.id)
- fund.state = .idle // move onto stack
- cash.updateFund(fund)
- }
- DispatchQueue.main.asyncAfter(deadline: .now() + (fastAnimations ? 0.6 : 1.0)) {
- checkStacks = value
- }
- }
- } else if isFlipped {
- withAnimation(.fly1) {
- print("*** start flipping:", value, fund.id)
- fund.state = .idle
- cash.updateFund(fund)
- }
- } else if shouldMorph {
- withAnimation(.fly1) {
- print("*** start morphing:", value, fund.id)
- fund.state = .isMorphing
- cash.updateFund(fund)
- }
- DispatchQueue.main.asyncAfter(deadline: .now() + (fastAnimations ? 0.6 : 1.0)) {
- withAnimation(.fly1) {
- print("*** end morphing:", value, fund.id)
- fund.state = .idle // move onto stack
- cash.updateFund(fund)
- }
- }
-// } else {
-// print(" ->OIMlayoutView.onAppear ignore \(value), \(fund.id)")
+ startFlying(fundID: fundID)
+ } else {
+// print(" ->OIMlayout ForEach fund.onAppear ignore \(value), \(fundID)")
}
}
}
}
.onChange(of: checkStacks) { value in
if value > 0 {
- print("*** check:", value)
+ symLog.log("*** onChange(of: checkStacks) \(value)")
var firstCheck = value
if let moreThan4 = cash.checkStacks(first: firstCheck) {
firstCheck = 0
-// withAnimation(.fly1) {
- cash.compactStacks(moreThan4)
-// }
+ cash.compactStacks(moreThan4)
}
checkStacks = 0
}
@@ -153,61 +160,81 @@ struct OIMlayoutView: View {
// MARK: -
@available(iOS 16.4, *)
struct OIMlayout: Layout {
-// struct CacheData {
-//// var maxHeight: CGFloat
-// var spaces: [CGSize]
-// }
-//
-// func computeMaxHeight(sorted: [LayoutSubview]) -> CGFloat {
-//
-// return sorted.map { $0.sizeThatFits(.unspecified) }.reduce(0) { max($0, $1.height) }
-// }
-//
-// func computeSpaces(_ sorted: [LayoutSubview]) -> [CGSize] {
-// //
-// return sorted.indices.map { idx in
-// guard idx < sorted.count - 1 else { return CGSize.zero }
-//
-// return
-// }
-// }
-//
-// func makeCache(subviews: Subviews) -> CacheData {
-// let sorted = sortByValue(subviews)
-// return CacheData(maxHeight: computeMaxHeight(sorted),
-// spaces: computeSpaces(subviews: sorted))
-// }
-//
-// func updateCache(_ cache: inout CacheData, subviews: Subviews) {
-// cache.maxHeight = computeMaxHeight(subviews: subviews)
-// cache.spaces = computeSpaces(subviews: subviews)
-// }
func sortByValue(_ subviews: LayoutSubviews) -> [LayoutSubview] {
// sorts DESCENDING - we render the denominations top-down
- subviews.sorted(by: { ($0.oimValue > $1.oimValue) ||
- ( ($0.oimValue == $1.oimValue) && ($0.oimID < $1.oimID) ) })
+ subviews.sorted(by: {
+ let state0 = $0.oimFundState
+ let state1 = $1.oimFundState
+ let value0 = $0.oimValue
+ let value1 = $1.oimValue
+ let flipV0 = $0.oimFlippedVal
+ let flipV1 = $1.oimFlippedVal
+ let id0 = $0.oimID
+ let id1 = $1.oimID
+ let isSameVal = value0 == value1
+ let id0isSmaller = id0 < id1
+ let id0isBigger = id1 < id0
+
+ let isMorphing1 = state1 == .position
+ || state1 == .morphing
+ || state1 == .morphed
+ if isMorphing1 {
+ if flipV1 > 0 {
+ let firstIsBigger = value0 > flipV1
+ let sameFlip = value0 == flipV1
+
+ let sameFlip_ID0smaller = sameFlip && id0isSmaller
+ let result = firstIsBigger || sameFlip_ID0smaller
+ return result
+ }
+ }
+
+ let isMorphing0 = state0 == .position
+ || state0 == .morphing
+ || state0 == .morphed
+ if isMorphing0 {
+ if flipV0 > 0 {
+ let firstIsBigger = flipV0 > value1
+ let sameFlip = value1 == flipV0
+
+ let sameFlip_ID0smaller = sameFlip && id0isSmaller
+ let result = firstIsBigger || sameFlip_ID0smaller
+ return result
+ }
+ }
+ let firstIsBigger = value0 > value1
+ // but keep the order of the IDs for identical values
+ let sameVal_ID0smaller = isSameVal && id0isSmaller
+ let result = firstIsBigger || sameVal_ID0smaller
+ return result
+ })
}
func computeSpaces(_ sorted: [LayoutSubview], _ max: Int = MAXSTACK) -> [Int] {
+ /// same algorithm as OIMcash.checkStacks
/// returns array of stackIndexes
/// 0 ==> next view starts a new stack
/// 1..3 ==> next view is on the stack
+ /// -1 ==> view is invisible, will be deleted
var spaces: [Int] = []
- let count = sorted.count
-
var stackIndex = 0
var lastValue: UInt64 = 0
for (index, subview) in sorted.enumerated() {
let value = subview.oimValue
let state = subview.oimFundState
+ let id = subview.oimID
+ var ignore = 0
if state == .shouldFly || state == .isFlying // flying can not go on the stack
- || state == .morphingIn // nor can morphing in...
- || state == .morphingOut { // ...or morphing out
+ || state == .position // nor can the position where we morph...
+ || state == .morphing // ...or the morphing fund...
+ || state == .morphed { // ...or morphed 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 {
+ ignore = -1
} else if lastValue != value { // different value?
lastValue = value // save this value for the next subview
stackIndex = 0 // start a new stack
@@ -217,7 +244,7 @@ struct OIMlayout: Layout {
stackIndex = 0 // stack is full, start a new one but keep the value
} }
if index > 0 {
- spaces.append(stackIndex)
+ spaces.append(ignore == 0 ? stackIndex : ignore)
}
}
return spaces
@@ -226,11 +253,11 @@ struct OIMlayout: Layout {
func accumulate(spaces: [Int], _ viewSizes: [CGSize], spacing: CGFloat, xOffset: CGFloat) -> CGFloat {
var accumulatedSpaces: CGFloat = .zero
for (idx, stackIndex) in spaces.enumerated() {
- if stackIndex > 0 {
+ if stackIndex > 0 { // next view is on the stack, add xOffset, subtract width
accumulatedSpaces += (xOffset - viewSizes[idx].width)
- } else {
+ } else if stackIndex == 0 { // start a new stack, add spacing
accumulatedSpaces += spacing
- }
+ } // else ignore view
}
return accumulatedSpaces
}
@@ -251,7 +278,7 @@ struct OIMlayout: Layout {
var result = CGFloat.zero
for (index, subview) in views.enumerated() {
let state = subview.oimFundState
- if state != .morphingOut {
+ if state != .moving && state != .hiding {
let size = sizes[index]
result += size.width
}
@@ -265,7 +292,6 @@ struct OIMlayout: Layout {
let sorted = sortByValue(subviews)
let viewSizes = sorted.map { $0.sizeThatFits(proposal) } // <- THIS takes time...
let maxHeight = viewSizes.reduce(0) { max($0, $1.height) } // get the max height
-// let viewWidths = viewSizes.reduce(0) { $0 + $1.width } // add up all widths
let viewWidths = accumulate(views: sorted, sizes: viewSizes) // add up all widths
let spaces = computeSpaces(sorted)
@@ -296,12 +322,12 @@ struct OIMlayout: Layout {
// // TODO: compute the inner-stack offsets
// spaceToFill -= viewWidths // might be negative
} else { // unspecified
- print("Yikes❗️ width only:", spaceToFill)
+// print("Yikes❗️ width only:", spaceToFill)
}
} else if var heightToFill = proposal.height {
- print("Yikes❗️ height only:", heightToFill)
+// print("Yikes❗️ height only:", heightToFill)
} else { // unspecified
- print("Yikes❗️ should NEVER happen:", proposal)
+// print("Yikes❗️ should NEVER happen:", proposal)
}
// while inner-stack offsets is not yet implemented, return ideal size
@@ -325,42 +351,43 @@ struct OIMlayout: Layout {
var morphPt = CGPoint.zero
for idx in sorted.indices {
let subview = sorted[idx]
+ let id = subview.oimID
let state = subview.oimFundState
- if state == .morphingIn || state == .isMorphing {
- print("morphing into ク\(subview.oimValue),\(subview.oimID) at", pt)
+ if state == .position || state == .morphing || state == .morphed {
+// 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 == .morphingOut {
+ if state == .moving || state == .hiding {
if morphPt != CGPoint.zero {
- print("morphing ク\(subview.oimValue),\(subview.oimID) from \(pt) to", morphPt)
+// print("morphing ク\(subview.oimValue) \(subview.oimID) \(state) from \(pt) to", morphPt)
subview.place(at: morphPt, anchor: .topLeading, proposal: proposal)
} else {
- print("Yikes: no morphing point for ク", subview.oimValue, subview.oimID)
+// symLog.log("Yikes: no morphing point for ク", subview.oimValue, subview.oimID, state)
}
- } else if state != .morphingIn && state != .isMorphing {
- print("placing ク\(subview.oimValue),\(subview.oimID) at", pt)
+// } else if state != .morphingIn && state != .morphedIn {
+// print("placing ク\(subview.oimValue),\(subview.oimID) at", pt)
}
if idx < sorted.count - 1 {
let space = spaces[idx]
let nextView = sorted[idx+1]
let state = nextView.oimFundState
- if state != .morphingOut {
+ if state != .moving && state != .hiding {
if space == 0 || isMax {
- // next subview starts a new stack
+// print("Start new stack for ク", nextView.oimValue, nextView.oimID, state)
let width = subview.sizeThatFits(proposal).width
pt.x += width + spacing
pt.y = bounds.minY
- } else {
- // netx subview is on this stack
+ } else if space > 0 {
+// print(" place on stack ク", nextView.oimValue, nextView.oimID, state)
pt.x += offset.x
pt.y += offset.y
+// } else {
+// print("ignore ク", nextView.oimValue, nextView.oimID, state)
}
} // place subviews morphing out at position of view morphing in
}
}
}
}
-
-
diff --git a/TalerWallet1/Views/OIM/OIMlineView.swift b/TalerWallet1/Views/OIM/OIMlineView.swift
@@ -7,11 +7,13 @@
*/
import SwiftUI
import taler_swift
+import SymLog
fileprivate let horzSpacing: CGFloat = 20
fileprivate let vertSpacing: CGFloat = 10
struct OIMlineView: View {
+ private let symLog = SymLogV(0)
let stack: CallStack
@Binding var amountVal: UInt64
@Binding var tappedVal: UInt64
@@ -40,11 +42,11 @@ struct OIMlineView: View {
let notes = OIMlayoutView(stack: stack.push(),
funds: cash.notes(),
amountVal: $amountVal,
- canEdit: true)
+ canEdit: canEdit)
let coins = OIMlayoutView(stack: stack.push(),
funds: cash.coins(),
amountVal: $amountVal,
- canEdit: true)
+ canEdit: canEdit)
LayoutThatFits([HStackLayout(alignment: .center), VStackLayout()]) {
notes
#if DEBUG
@@ -115,26 +117,25 @@ struct OIMlineView: View {
}
.onChange(of: tappedVal) { newVal in
if newVal > 0 {
- print(">>tapped ", newVal)
+ symLog.log(">>tapped \(newVal)")
tappedVal = 0
- let ms = fastAnimations ? 250 : 500
DispatchQueue.main.async { // next layout cycle
- withAnimation(.fly1) {
- if #available(iOS 16.4, *) {
- print("\n>>addCash", newVal)
+ withAnimation(.move1) {
+ if #available(iOS 16.0, *) {
+ symLog.log(">>addCash \(newVal)")
cash.addCash(value: newVal)
amountVal += newVal // update directly
} else {
- print("\n>>start flying", newVal, flying)
+ symLog.log("\n>>start flying \(newVal) \(flying)")
myTappedVal = newVal
}
}
- if #unavailable(iOS 16.4) {
- print("\n>>reset flying", newVal, flying)
+ if #unavailable(iOS 16.0) {
+ symLog.log("\n>>reset flying \(newVal) \(flying)")
flying = 0 // remove immediately after it flew in, but outside of the animation block
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) {
- print("\n>>dissolve", newVal, flying)
+ DispatchQueue.main.asyncAfter(deadline: .now() + cash.delay) {
+ symLog.log("\n>>dissolve \(newVal) \(flying)")
withAnimation(.basic1) {
amountVal += newVal // update after value flew in
myTappedVal = 0