commit 5ccb4e6cf8517a24bc6105e1cb4c4ecc0062669e
parent 2b28cebc2cb94ea4e2d4080e819deaf5b872c087
Author: Marc Stibane <marc@taler.net>
Date: Wed, 16 Apr 2025 14:34:29 +0200
morphing
Diffstat:
3 files changed, 113 insertions(+), 47 deletions(-)
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -12,9 +12,12 @@ let MAXSTACK = 4
enum FundState: Int {
case idle
- case shouldFly
- case isFlying
- case flipped
+ 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 //
}
/// data structure for a cash item on the table
@@ -52,7 +55,7 @@ class OIMcash: ObservableObject {
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) ) })
+ ( ($0.value == $1.value) && ($0.id < $1.id) ) }) // but keep the order of the IDs
}
func checkStacks(first firstCheck: UInt64, _ max: Int = MAXSTACK) -> UInt64? {
@@ -91,7 +94,7 @@ class OIMcash: ObservableObject {
for var fund in sorted {
if fund.value == stackValue && counter > 0 {
withAnimation(.basic1) {
- fund.state = .flipped
+ fund.state = .morphingOut
updateFund(fund)
}
toDelete.append(fund)
@@ -100,19 +103,30 @@ class OIMcash: ObservableObject {
}
}
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)
}
}
- withAnimation(.basic1) {
- while amount >= biggerDenom {
- self.addCash(value: biggerDenom, .flipped)
- amount -= biggerDenom
+ withAnimation(.fly1) {
+ for var fund in added {
+ print("finish morphing", fund.id)
+ fund.state = .idle
+ self.updateFund(fund)
}
}
+
if amount > 0 {
- print(" ❗️Yikes: convert failed", amount)
+ print(" ❗️Yikes: convert failed, leftover:", amount)
}
}
return counter == 0
@@ -174,16 +188,18 @@ class OIMcash: ObservableObject {
return funds.filter { $0.value <= firstCoinVal }
}
- func addCash(value: UInt64, _ newState: FundState = .shouldFly) {
+ func addCash(value: UInt64, _ newState: FundState = .shouldFly) -> OIMfund {
let fund = OIMfund(id: ticker, value: value, state: newState)
ticker += 1
+ print(">>adding", value, fund.id)
funds.append(fund)
+ return fund
}
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 == .flipped }) {
+ } 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
@@ -108,7 +108,7 @@ struct OIMcurrencyButton: View {
name: name,
availableVal: availableVal,
canEdit: canEdit,
- isFlipped: fund.state == .flipped,
+ isFlipped: fund.state == .flipping,
pct: pct,
action: action))
}
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -60,12 +60,13 @@ struct OIMlayoutView: View {
let value = fund.value
let fundState = fund.state
let shouldFly = fundState == .shouldFly
- let isFlipped = fundState == .flipped
+ let isFlipped = fundState == .flipping
+ let shouldMorph = fundState == .morphingIn
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: cash.currency,
availableVal: value,
- canEdit: canEdit && !isFlipped, // && isTop,
+ canEdit: canEdit && !isFlipped,
pct: shouldFly ? 0.0 : 1.0
) { // remove on button press
if amountVal >= fund.value {
@@ -75,7 +76,7 @@ struct OIMlayoutView: View {
amountVal = 0
}
withAnimation(.basic1) {
- fund.state = .flipped // button animates itself anyway
+ fund.state = .flipping
cash.updateFund(fund)
}
DispatchQueue.main.async {
@@ -95,14 +96,14 @@ struct OIMlayoutView: View {
if shouldFly {
// print(" ->OIMlayoutView.onAppear stop matching \(value), \(fund.id)")
withAnimation(.fly1) {
- print("*** start flying:", value)
+ 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.state = .idle
+ 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)) {
@@ -111,10 +112,23 @@ struct OIMlayoutView: View {
}
} else if isFlipped {
withAnimation(.fly1) {
- print("*** start flipping:", value)
+ 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)")
}
@@ -188,16 +202,19 @@ struct OIMlayout: Layout {
for (index, subview) in sorted.enumerated() {
let value = subview.oimValue
let state = subview.oimFundState
- if state == .shouldFly || state == .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?
- lastValue = value // save this value for the next subview
- stackIndex = 0 // start a new stack
+ if state == .shouldFly || state == .isFlying // flying can not go on the stack
+ || state == .morphingIn // nor can morphing in...
+ || state == .morphingOut { // ...or morphing out
+ // 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 lastValue != value { // different value?
+ lastValue = value // save this value for the next subview
+ stackIndex = 0 // start a new stack
} else {
- stackIndex += 1 // Yay, we found one to add to this stack
- if stackIndex == max { // max 4 subviews per stack
- stackIndex = 0 // stack is full, start a new one but keep the value
+ stackIndex += 1 // Yay, we found one to add to this stack
+ if stackIndex == max { // max 4 subviews per stack
+ stackIndex = 0 // stack is full, start a new one but keep the value
} }
if index > 0 {
spaces.append(stackIndex)
@@ -206,7 +223,7 @@ struct OIMlayout: Layout {
return spaces
}
- func accumulate(_ spaces: [Int], _ viewSizes: [CGSize], spacing: CGFloat, xOffset: CGFloat) -> CGFloat {
+ func accumulate(spaces: [Int], _ viewSizes: [CGSize], spacing: CGFloat, xOffset: CGFloat) -> CGFloat {
var accumulatedSpaces: CGFloat = .zero
for (idx, stackIndex) in spaces.enumerated() {
if stackIndex > 0 {
@@ -230,13 +247,26 @@ struct OIMlayout: Layout {
}
}
+ func accumulate(views: [LayoutSubview], sizes: [CGSize]) -> CGFloat {
+ var result = CGFloat.zero
+ for (index, subview) in views.enumerated() {
+ let state = subview.oimFundState
+ if state != .morphingOut {
+ let size = sizes[index]
+ result += size.width
+ }
+ }
+ return result
+ }
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard subviews.count > 0 else { return CGSize.zero }
let (spacing, offset) = spacing(for: proposal)
let sorted = sortByValue(subviews)
let viewSizes = sorted.map { $0.sizeThatFits(proposal) } // <- THIS takes time...
- let viewWidths = viewSizes.reduce(0) { $0 + $1.width } // add up all widths
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)
let maxStackIndex = spaces.reduce(0) { max($0, $1) }
@@ -245,19 +275,19 @@ struct OIMlayout: Layout {
var stackHeight: CGFloat = .zero
switch proposal {
case .infinity: // max size: just place all views in one row
- print("MAX:", proposal)
+// print("MAX:", proposal)
accumulatedSpaces = spacing * CGFloat(sorted.count - 1)
break
case .zero: // minimum size:
- print("min:", proposal)
+// print("min:", proposal)
// all stacks with offset x = 8, y = 20
stackHeight = offset.y * CGFloat(maxStackIndex)
- accumulatedSpaces = accumulate(spaces, viewSizes, spacing: spacing, xOffset: offset.x)
+ accumulatedSpaces = accumulate(spaces: spaces, viewSizes, spacing: spacing, xOffset: offset.x)
break
case .unspecified: // => ideal size
- print("Ideal:", proposal)
+// print("Ideal:", proposal)
stackHeight = offset.y * CGFloat(maxStackIndex)
- accumulatedSpaces = accumulate(spaces, viewSizes, spacing: spacing, xOffset: offset.x)
+ accumulatedSpaces = accumulate(spaces: spaces, viewSizes, spacing: spacing, xOffset: offset.x)
break
default:
if var spaceToFill = proposal.width {
@@ -276,7 +306,7 @@ struct OIMlayout: Layout {
// while inner-stack offsets is not yet implemented, return ideal size
stackHeight = offset.y * CGFloat(maxStackIndex)
- accumulatedSpaces = accumulate(spaces, viewSizes, spacing: spacing, xOffset: offset.x)
+ accumulatedSpaces = accumulate(spaces: spaces, viewSizes, spacing: spacing, xOffset: offset.x)
break
}
let result = CGSize(width: viewWidths + accumulatedSpaces,
@@ -292,22 +322,42 @@ struct OIMlayout: Layout {
let isMax = proposal == .infinity
var pt = CGPoint(x: bounds.minX, y: bounds.minY)
+ var morphPt = CGPoint.zero
for idx in sorted.indices {
let subview = sorted[idx]
+ let state = subview.oimFundState
+ if state == .morphingIn || state == .isMorphing {
+ print("morphing into ク\(subview.oimValue),\(subview.oimID) at", pt)
+ morphPt = pt
+ }
subview.place(at: pt, anchor: .topLeading, proposal: proposal)
+ if state == .morphingOut {
+ if morphPt != CGPoint.zero {
+ print("morphing ク\(subview.oimValue),\(subview.oimID) from \(pt) to", morphPt)
+ subview.place(at: morphPt, anchor: .topLeading, proposal: proposal)
+ } else {
+ print("Yikes: no morphing point for ク", subview.oimValue, subview.oimID)
+ }
+ } else if state != .morphingIn && state != .isMorphing {
+ print("placing ク\(subview.oimValue),\(subview.oimID) at", pt)
+ }
if idx < sorted.count - 1 {
let space = spaces[idx]
- if space == 0 || isMax {
- // next subview starts a new stack
- let width = subview.sizeThatFits(proposal).width
- pt.x += width + spacing
- pt.y = bounds.minY
- } else {
- // netx subview is on this stack
- pt.x += offset.x
- pt.y += offset.y
- }
+ let nextView = sorted[idx+1]
+ let state = nextView.oimFundState
+ if state != .morphingOut {
+ if space == 0 || isMax {
+ // next subview starts a new stack
+ let width = subview.sizeThatFits(proposal).width
+ pt.x += width + spacing
+ pt.y = bounds.minY
+ } else {
+ // netx subview is on this stack
+ pt.x += offset.x
+ pt.y += offset.y
+ }
+ } // place subviews morphing out at position of view morphing in
}
}
}