taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit ec7a20acb22bd9ff515b51906b1995135ff7edb2
parent 66476c749505702fb483b5fd3a83c203caa57d90
Author: Marc Stibane <marc@taler.net>
Date:   Tue, 15 Apr 2025 11:55:04 +0200

shouldFly

Diffstat:
MTalerWallet1/Views/OIM/OIMcash.swift | 9+++++----
MTalerWallet1/Views/OIM/OIMlayout.swift | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
MTalerWallet1/Views/OIM/OIMlineView.swift | 13+++++--------
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 } }