taler-ios

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

commit 252a53c528c5f717ab694d23ab4c731ff7805d97
parent a18272cce61d595334685048e0f3d3ad197ffae0
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 19 Apr 2025 00:26:26 +0200

finetuning

Diffstat:
MTalerWallet1/Views/OIM/OIMcash.swift | 93++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
MTalerWallet1/Views/OIM/OIMcurrencyButton.swift | 93++++++++++++++++++++++++++++++++++++++-----------------------------------------
MTalerWallet1/Views/OIM/OIMlayout.swift | 21++++++++++++---------
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)