commit ec7a20acb22bd9ff515b51906b1995135ff7edb2
parent 66476c749505702fb483b5fd3a83c203caa57d90
Author: Marc Stibane <marc@taler.net>
Date: Tue, 15 Apr 2025 11:55:04 +0200
shouldFly
Diffstat:
3 files changed, 177 insertions(+), 55 deletions(-)
diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift
@@ -10,7 +10,8 @@ import taler_swift
enum FundState: Int {
case idle
- case flying
+ case shouldFly
+ case isFlying
case flipped
}
@@ -21,8 +22,8 @@ public struct OIMfund: Identifiable, Equatable, Hashable {
var state: FundState
var targetID: String {
- String(state == .flying ? -Int(value) // match sourceID
- : id)
+ String(state == .shouldFly ? -Int(value) // match sourceID
+ : id)
}
}
public typealias OIMfunds = [OIMfund]
@@ -57,7 +58,7 @@ class OIMcash: ObservableObject {
}
func addCash(_ value: UInt64) {
- let fund = OIMfund(id: ticker, value: value, state: .flying)
+ let fund = OIMfund(id: ticker, value: value, state: .shouldFly)
ticker += 1
funds.append(fund)
}
diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift
@@ -58,14 +58,14 @@ struct OIMlayoutView: View {
let value = fund.value
let fundState = fund.state
- let isFlying = fundState == .flying
+ let shouldFly = fundState == .shouldFly
let isFlipped = fundState == .flipped
OIMcurrencyButton(stack: stack.push(),
fund: fund,
currency: cash.currency,
availableVal: value,
canEdit: canEdit && !isFlipped, // && isTop,
- pct: isFlying ? 0.0 : 1.0
+ pct: shouldFly ? 0.0 : 1.0
) { // remove on button press
if amountVal >= fund.value {
amountVal -= fund.value
@@ -84,21 +84,30 @@ struct OIMlayoutView: View {
}
}
}
+ .zIndex(Double(fund.id))
.id(fund.id)
.oimValue(value)
.oimID(fund.id)
.oimFundState(fundState)
.matchedGeometryEffect(id: fund.targetID, in: wrapper.namespace, isSource: false) // !isFlying
.onAppear {
- if isFlying {
+ if shouldFly {
// print(" ->OIMlayoutView.onAppear stop matching \(value), \(fund.id)")
withAnimation(.fly1) {
- // OIMnoteV appeared at the position of the currency button, now start flying
- fund.state = .idle // switch off matching
+ print("*** start flying:", value)
+ 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
+ cash.updateFund(fund)
+ }
+ }
} else if isFlipped {
withAnimation(.fly1) {
+ print("*** start flipping:", value)
fund.state = .idle
cash.updateFund(fund)
}
@@ -113,62 +122,177 @@ struct OIMlayoutView: View {
// MARK: -
@available(iOS 16.4, *)
struct OIMlayout: Layout {
- struct CacheData {
- var maxHeight: CGFloat
- var spaces: [CGFloat]
+// 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] {
+ subviews.sorted(by: { ($0.oimValue > $1.oimValue) ||
+ ( ($0.oimValue == $1.oimValue) && ($0.oimID < $1.oimID) ) })
}
- var spacing: CGFloat? = nil
+ func computeSpaces(_ proposal: ProposedViewSize, _ sorted: [LayoutSubview], _ max: Int = 3) -> [Int] {
+ /// returns array of stackIndexes
+ /// 0 ==> next view starts a new stack
+ /// 1..3 ==> next view is on the stack
+ var spaces: [Int] = []
- func makeCache(subviews: Subviews) -> CacheData {
- return CacheData(maxHeight: computeMaxHeight(subviews: subviews),
- spaces: computeSpaces(subviews: subviews))
- }
+ let count = sorted.count
- func updateCache(_ cache: inout CacheData, subviews: Subviews) {
- cache.maxHeight = computeMaxHeight(subviews: subviews)
- cache.spaces = computeSpaces(subviews: subviews)
+ var stackIndex = 0
+ var lastValue: UInt64 = 0
+ 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
+ } else if stackIndex == max { // max 4 subviews per stack
+ stackIndex = 0 // stack is full, start a new one but keep the value
+ } else {
+ stackIndex += 1 // Yay, we found one to add to this stack
+ }
+ if index > 0 {
+ spaces.append(stackIndex)
+ }
+ }
+ return spaces
}
- func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
- let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
- let accumulatedWidths = idealViewSizes.reduce(0) { $0 + $1.width }
- let accumulatedSpaces = cache.spaces.reduce(0) { $0 + $1 }
+ func accumulate(_ spaces: [Int], _ viewSizes: [CGSize], spacing: CGFloat, xOffset: CGFloat) -> CGFloat {
+ var accumulatedSpaces: CGFloat = .zero
+ for (idx, stackIndex) in spaces.enumerated() {
+ if stackIndex > 0 {
+ accumulatedSpaces += (xOffset - viewSizes[idx].width)
+ } else {
+ accumulatedSpaces += spacing
+ }
+ }
+ return accumulatedSpaces
+ }
- return CGSize(width: accumulatedSpaces + accumulatedWidths,
- height: cache.maxHeight)
+ func spacing(for proposal: ProposedViewSize) -> (CGFloat, CGPoint) {
+ switch proposal {
+ case .infinity: // max size: just place all views in one row
+ (10, CGPoint(x: 10, y: 0))
+ case .zero: // minimum size: tight spacing
+ (8, CGPoint(x: 8, y: 20))
+ default: // TODO: compute offset - in the meantime => ideal size
+// case .unspecified: // => ideal size
+ (10, CGPoint(x: 16, y: 16))
+ }
}
- func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) {
- var pt = CGPoint(x: bounds.minX, y: bounds.minY)
+ 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 sorted = subviews.sorted(by: { ($0.oimValue > $1.oimValue) ||
- ( ($0.oimValue == $1.oimValue) && ($0.oimID < $1.oimID) ) })
- for idx in sorted.indices {
- sorted[idx].place(at: pt, anchor: .topLeading, proposal: .unspecified)
+ let spaces = computeSpaces(proposal, sorted)
+ let maxStackIndex = spaces.reduce(0) { max($0, $1) }
- if idx < sorted.count - 1 {
- let width = sorted[idx].sizeThatFits(.unspecified).width
- pt.x += width + cache.spaces[idx]
- }
+ var accumulatedSpaces: CGFloat = .zero
+ var stackHeight: CGFloat = .zero
+ switch proposal {
+ case .infinity: // max size: just place all views in one row
+ print("MAX:", proposal)
+ accumulatedSpaces = spacing * CGFloat(sorted.count - 1)
+ break
+ case .zero: // minimum size:
+ 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)
+ break
+ case .unspecified: // => ideal size
+ print("Ideal:", proposal)
+ stackHeight = offset.y * CGFloat(maxStackIndex)
+ accumulatedSpaces = accumulate(spaces, viewSizes, spacing: spacing, xOffset: offset.x)
+ break
+ default:
+ if var spaceToFill = proposal.width {
+ if var heightToFill = proposal.height {
+ print("Yikes❗️ width + height:", spaceToFill, heightToFill)
+// // TODO: compute the inner-stack offsets
+// spaceToFill -= viewWidths // might be negative
+ } else { // unspecified
+ print("Yikes❗️ width only:", spaceToFill)
+ }
+ } else if var heightToFill = proposal.height {
+ print("Yikes❗️ height only:", heightToFill)
+ } else { // unspecified
+ print("Yikes❗️ should NEVER happen:", proposal)
+ }
+
+ // 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)
+ break
}
+ let result = CGSize(width: viewWidths + accumulatedSpaces,
+ height: maxHeight + stackHeight)
+ print(" sizeThatFits:", result)
+ return result
}
- func computeSpaces(subviews: LayoutSubviews) -> [CGFloat] {
- if let spacing {
- return Array<CGFloat>(repeating: spacing, count: subviews.count - 1)
- } else {
- return subviews.indices.map { idx in
- guard idx < subviews.count - 1 else { return CGFloat(0) }
+ func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
+ let (spacing, offset) = spacing(for: proposal)
+ let sorted = sortByValue(subviews)
+ let spaces = computeSpaces(proposal, sorted)
+ let isMax = proposal == .infinity
+
+ var pt = CGPoint(x: bounds.minX, y: bounds.minY)
+ for idx in sorted.indices {
+ let subview = sorted[idx]
+ subview.place(at: pt, anchor: .topLeading, proposal: proposal)
- return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
+ 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
+ }
}
}
}
-
- func computeMaxHeight(subviews: LayoutSubviews) -> CGFloat {
- return subviews.map { $0.sizeThatFits(.unspecified) }.reduce(0) { max($0, $1.height) }
- }
}
diff --git a/TalerWallet1/Views/OIM/OIMlineView.swift b/TalerWallet1/Views/OIM/OIMlineView.swift
@@ -118,8 +118,8 @@ struct OIMlineView: View {
if newVal > 0 {
tappedVal = 0
let ms = debugAnimations ? 1250 : 250
- // next cycle
- DispatchQueue.main.async {
+
+ DispatchQueue.main.async { // next layout cycle
withAnimation(.fly1) {
if #available(iOS 16.4, *) {
print("\n>>addCash", newVal)
@@ -130,16 +130,13 @@ struct OIMlineView: View {
myTappedVal = newVal
}
}
-
- print("\n>>reset flying", newVal, flying)
- flying = 0 // remove immediately after it flew in, but outside of the animation block
-
-
if #unavailable(iOS 16.4) {
+ print("\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)
withAnimation(.basic1) {
- amountVal += newVal
+ amountVal += newVal // update after value flew in
myTappedVal = 0
}
}