taler-ios

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

commit b3866accb807914b62975b5c8706e03d19e9c6bf
parent a8635e9b4c7413bb2cc6222fd7fe3709775ddc9d
Author: Marc Stibane <marc@taler.net>
Date:   Sun, 27 Apr 2025 23:59:29 +0200

Use layout for iOS16+

Diffstat:
ATalerWallet1/Views/OIM/OIM15Views.swift | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/OIM/OIMView.swift | 28+++++++++++++++-------------
MTalerWallet1/Views/OIM/OIMcash.swift | 76+++++++++++++++++++++++++++++-----------------------------------------------
DTalerWallet1/Views/OIM/OIMcoinStackV.swift | 183-------------------------------------------------------------------------------
DTalerWallet1/Views/OIM/OIMcoinsView.swift | 86-------------------------------------------------------------------------------
MTalerWallet1/Views/OIM/OIMcurrencyButton.swift | 17++++++++++-------
MTalerWallet1/Views/OIM/OIMcurrencyScroller.swift | 19++++++++-----------
DTalerWallet1/Views/OIM/OIMcurrencyViews2.swift | 71-----------------------------------------------------------------------
MTalerWallet1/Views/OIM/OIMlayout.swift | 10+++++-----
ATalerWallet1/Views/OIM/OIMlineView.swift | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DTalerWallet1/Views/OIM/OIMlineViews.swift | 199-------------------------------------------------------------------------------
11 files changed, 413 insertions(+), 622 deletions(-)

diff --git a/TalerWallet1/Views/OIM/OIM15Views.swift b/TalerWallet1/Views/OIM/OIM15Views.swift @@ -0,0 +1,190 @@ +/* + * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. + * See LICENSE.md + */ +/** + * @author Marc Stibane + */ +import SwiftUI +import SymLog +//import taler_swift + +// These views are for iOS 15 only - remove once iOS 16 is the Minimum Deployment +// MARK: - +// renders a stack of (identical) banknotes with offset 10,20 +struct OIMnoteStackV: View { + private let symLog = SymLogV(0) + let stack: CallStack + let value: UInt64 + let count: Int + let currency: OIMcurrency + let tappedVal: UInt64 + @Binding var flying: UInt64 + let canEdit: Bool + let action: () -> Void + + @EnvironmentObject private var wrapper: NamespaceWrapper + + var body: some View { +// let _ = Self._printChanges() + let maxIndex = count - 1 + ZStack { + ForEach(0...maxIndex, id: \.self) { index in + let match = tappedVal == value && index == maxIndex + let isFlying = flying > 0 + let matchNotFlying = match && !isFlying + let isTarget = matchNotFlying // && isTop + let targetID = isTarget ? String(-Int(value)) + : String(value) + "+" + String(index) + 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) + OIMcurrencyButton(stack: stack.push(), +// value: value, + fund: fund, + currency: currency, + availableVal: value, + canEdit: canEdit, +// isFlipped: false, // TODO: Flip coin + pct: match ? 0.0 : 1.0, + action: action) + .offset(x: xOffset, y: yOffset) + .matchedGeometryEffect(id: targetID, in: wrapper.namespace, isSource: false) + .onAppear { + print("start flying \(targetID)") + withAnimation(.fly1) { + flying = value // start flying + } + } + .zIndex(5) + } + } + .padding(.trailing, CGFloat(10 * maxIndex)) + .padding(.bottom, CGFloat(20 * maxIndex)) + } +} +// MARK: - +// renders a stack of (identical) coins with offset size/16 +struct OIMcoinStackV: View { + let stack: CallStack + let value: UInt64 + let count: Int + let currency: OIMcurrency + let tappedVal: UInt64 + @Binding var flying: UInt64 + let canEdit: Bool + let action: () -> Void + + @EnvironmentObject private var wrapper: NamespaceWrapper + + var body: some View { + let maxIndex = count - 1 + if let size = currency.coinSize(value) { + let offset = size / 16 + ZStack { + ForEach(0...maxIndex, id: \.self) { index in + let match = tappedVal == value && index == maxIndex + let isFlying = flying > 0 + let matchNotFlying = match && !isFlying + let isTarget = matchNotFlying // && isTop + let targetID = isTarget ? String(-Int(value)) + : String(value) + "+" + String(index) + 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 + OIMcurrencyButton(stack: stack.push(), +// value: value, + fund: fund, + currency: currency, + availableVal: value, + canEdit: canEdit, +// isFlipped: false, // TODO: Flip coin + pct: match ? 0.0 : 1.0, + action: action) + .offset(x: xOffset, y: yOffset) + .matchedGeometryEffect(id: targetID, in: wrapper.namespace, isSource: false) + .onAppear { + print("start flying \(targetID)") + withAnimation(.fly1) { + flying = value // start flying + } + } + .zIndex(5) + } + } + .padding(.trailing, offset * CGFloat(maxIndex)) + .padding(.bottom, offset * CGFloat(maxIndex)) + } + } +} +// MARK: - +// renders a spread of banknote-stacks in 1 row +struct OIMnotesView1: View { + let stack: CallStack + let spread: OIMdenominations + let currency: OIMcurrency + @Binding var amountVal: UInt64 + let tappedVal: UInt64 + @Binding var flying: UInt64 + let canEdit: Bool + + var body: some View { +// let _ = Self._printChanges() + let nrOfNotes = currency.bankNotes.count - 1 + HStack(alignment: .center, spacing: 10) { + ForEach(0...nrOfNotes, id: \.self) { index in + let value = currency.bankNotes[index] + let shouldFly = tappedVal == value + let count = spread[index] + (shouldFly ? 1 : 0) + if count > 0 { + OIMnoteStackV(stack: stack.push(), + value: value, + count: Int(count), + currency: currency, + tappedVal: tappedVal, + flying: $flying, + canEdit: canEdit + ) { + withAnimation(.fly1) { + amountVal -= value // remove on button press + } } } + } // ForEach + } // HStack + } +} +// MARK: - +// renders a spread of coin-stacks in 1 row +struct OIMcoinsView1: View { + let stack: CallStack + let spread: OIMdenominations + let currency: OIMcurrency + @Binding var amountVal: UInt64 + let tappedVal: UInt64 + @Binding var flying: UInt64 + let canEdit: Bool + + var body: some View { + let nrOfCoins = currency.bankCoins.count - 1 + HStack(alignment: .top, spacing: 10) { + ForEach(0...nrOfCoins, id: \.self) { index in + let value = currency.bankCoins[index] + let shouldFly = tappedVal == value + let count = spread[index] + (shouldFly ? 1 : 0) + if count > 0 { + OIMcoinStackV(stack: stack.push(), + value: value, + count: Int(count), + currency: currency, + tappedVal: tappedVal, + flying: $flying, + canEdit: canEdit + ) { + withAnimation(Animation.easeIn1) { + amountVal -= value + } } } + } // ForEach + } // HStack + } +} diff --git a/TalerWallet1/Views/OIM/OIMView.swift b/TalerWallet1/Views/OIM/OIMView.swift @@ -84,10 +84,10 @@ struct OIMView: View { OIMbackground(amount: amount, currencyName: currency.noteBase) { VStack { Spacer() - OIMlineViews(stack: stack.push(), - amountVal: $amountVal, - tappedVal: $tappedVal, - canEdit: false) + OIMlineView(stack: stack.push(), + amountVal: $amountVal, + tappedVal: $tappedVal, + canEdit: false) .matchedGeometryEffect(id: "OIMline", in: wrapper.namespace,isSource: true) Spacer() actions @@ -121,10 +121,10 @@ struct OIMPayView: View { OIMbackground(amount: amount, currencyName: currency.noteBase) { VStack { Spacer() - OIMlineViews(stack: stack.push(), - amountVal: $amountVal, - tappedVal: $tappedVal, - canEdit: true) + OIMlineView(stack: stack.push(), + amountVal: $amountVal, + tappedVal: $tappedVal, + canEdit: true) .matchedGeometryEffect(id: "OIMline", in: wrapper.namespace,isSource: true) Spacer() }.border(.red) @@ -176,10 +176,10 @@ struct OIMEditView: View { ZStack { // without this, money would fly below the scroller VStack { // even though this is the only item in the ZStack Spacer() - OIMlineViews(stack: stack.push(), - amountVal: $amountVal, - tappedVal: $tappedVal, - canEdit: true) + OIMlineView(stack: stack.push(), + amountVal: $amountVal, + tappedVal: $tappedVal, + canEdit: true) .matchedGeometryEffect(id: "OIMline", in: wrapper.namespace,isSource: true) .zIndex(1) // make notes fly from topZ .onChange(of: amountVal) { newVal in @@ -201,6 +201,9 @@ struct OIMEditView: View { } } .border(.red) +// .if(.iOS15) { view in +// view.ignoresSafeArea(edges: .bottom) // sadly, this does not work, because there is always a bottom bar :-( +// } } // ZStack } .environmentObject(cash) @@ -242,7 +245,6 @@ struct OIMSubjectView: View { }.task { } - } } diff --git a/TalerWallet1/Views/OIM/OIMcash.swift b/TalerWallet1/Views/OIM/OIMcash.swift @@ -10,37 +10,28 @@ import taler_swift enum FundState: Int { case idle - case shaking case flying case flipped } /// data structure for a cash item on the table public struct OIMfund: Identifiable, Equatable, Hashable { + public let id: Int // support multiple funds with the same value let value: UInt64 - var fundState: FundState - public let id: UUID // support multiple funds with the same value + var state: FundState - var match: String { - if fundState == .flying { - return String(value) - } - return String("\(id)") -// return String("-\(value)") + var targetID: String { + String(state == .flying ? -Int(value) // match sourceID + : id) } } public typealias OIMfunds = [OIMfund] -//public struct OIMfunds: Identifiable, Hashable { -// var funds: [OIMfund] -// public let id: UUID // support multiple funds with the same value -//} // MARK: - class OIMcash: ObservableObject { + var ticker = 0 // increment each time a fund is added (or melted from others) var currency: OIMcurrency - @Published var shake: UInt64 = 0 @Published var funds: OIMfunds = [] -// @Published var funds = OIMfunds(funds: [], id: UUID()) @AppStorage("sierraLeone") var sierraLeone: Bool = false @@ -55,47 +46,40 @@ class OIMcash: ObservableObject { } } - func insertFund(_ item: OIMfund) { - var didInsert = false -// for (index, fund) in funds.funds.enumerated() { - for (index, fund) in funds.enumerated() { - if !didInsert { - if fund.value < item.value { - funds.insert(item, at: index) -// funds.funds.insert(item, at: index) - didInsert = true - break - } - } - } - if !didInsert { - funds.append(item) -// funds.funds.append(item) - } + func notes() -> OIMfunds { + let firstCoinVal = currency.bankCoins[0] + return funds.filter { $0.value > firstCoinVal } + } + + func coins() -> OIMfunds { + let firstCoinVal = currency.bankCoins[0] + return funds.filter { $0.value <= firstCoinVal } } func addCash(_ value: UInt64) { - let fund = OIMfund(value: value, fundState: .flying, id: UUID()) - insertFund(fund) + let fund = OIMfund(id: ticker, value: value, state: .flying) + ticker += 1 + funds.append(fund) } - func removeCash(value: UInt64) { - if let index = funds.lastIndex(where: { $0.value == value && $0.fundState == .flipped }) { -// if let index = funds.funds.lastIndex(where: { $0.value == value && $0.fundState == .idle }) { + func removeCash(id: Int, value: UInt64) { + if let index = funds.firstIndex(where: { $0.id == id }) { + funds.remove(at: index) + } else if let index = funds.lastIndex(where: { $0.value == value && $0.state == .flipped }) { funds.remove(at: index) -// funds.funds.remove(at: index) } else if let index = funds.firstIndex(where: { $0.value == value }) { -// } else if let index = funds.funds.firstIndex(where: { $0.value == value }) { funds.remove(at: index) -// funds.funds.remove(at: index) } } func updateFund(_ item: OIMfund) { let itemID = item.id - funds = funds.map { -// funds.funds = funds.funds.map { - $0.id == itemID ? item : $0 + if let index = funds.firstIndex(where: { $0.id == itemID }) { + funds[index] = item + } else { + funds = funds.map { + $0.id == itemID ? item : $0 + } } } @@ -117,7 +101,6 @@ class OIMcash: ObservableObject { func update(_ notesCoins: OIMdenominations, currencyNotesCoins: OIMdenominations) -> Bool { var array = funds -// var array = funds.funds var changed = false for (index, value) in currencyNotesCoins.enumerated() { let wanted = notesCoins[index] // number of notes which should be shown @@ -134,8 +117,8 @@ class OIMcash: ObservableObject { count -= 1 } while count < wanted { -// let note = OIMcashItem(value: value, pos: -1, isFlying: false, shake: false, id: UUID()) - let note = OIMfund(value: value, fundState: .idle, id: UUID()) + let note = OIMfund(id: ticker, value: value, state: .idle) + ticker += 1 print("update: add \(note.value)") array.append(note) changed = true @@ -144,7 +127,6 @@ class OIMcash: ObservableObject { } if changed { funds = array -// funds.funds = array } return changed } diff --git a/TalerWallet1/Views/OIM/OIMcoinStackV.swift b/TalerWallet1/Views/OIM/OIMcoinStackV.swift @@ -1,183 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI - -// MARK: - -// renders a stack of (identical) banknotes with offset 10,20 -//struct OIMnoteStackV: View { -// let stack: CallStack -// let value: Int -// let count: Int -// let tappedVal: Int -// @Binding var flying: Int -// let canEdit: Bool -// let action: () -> Void -// -// @EnvironmentObject private var cash: OIMcash -// @EnvironmentObject private var wrapper: NamespaceWrapper -// -// var body: some View { -//// let _ = Self._printChanges() -// let maxIndex = count - 1 -// ZStack { -// ForEach(0...maxIndex, id: \.self) { index in -// let match = tappedVal == value && index == maxIndex -// let isFlying = flying > 0 -// let id = match && !isFlying ? value : -value -// let xOffset = CGFloat(10 * index) -// let yOffset = CGFloat(20 * index) -// let shakeOffset: CGFloat = (cash.shake != 0) && !match ? .random(in: 10...25) -// : .zero -// let direction = value < tappedVal -//// let _ = print("id \(id), flying \(flying), shaking \(shakeOffset)") -// OIMcurrencyButton(value: value, -//// currency: currency, -// availableVal: value, -// canEdit: canEdit, -// canFlip: true, -// pct: match ? 0.0 : 1.0, -// action: action) -// .offset(x: xOffset + (direction ? shakeOffset : -shakeOffset), -// y: yOffset) -// .matchedGeometryEffect(id: id, in: wrapper.namespace, isSource: false) -// .onAppear { -//// print("start flying \(id), shaking \(shakeOffset)") -// withAnimation(.fly1) { -// flying = value // start flying -// } -// } -// } -// } -// .padding(.trailing, CGFloat(10 * maxIndex)) -// .padding(.bottom, CGFloat(20 * maxIndex)) -// } -//} -// MARK: - -// renders a stack of (identical) coins with offset size/16 -struct OIMcoinStackV1: View { - let stack: CallStack - let value: UInt64 - let count: Int - let currency: OIMcurrency - let tappedVal: UInt64 - @Binding var flying: UInt64 - let shake: Bool - let canEdit: Bool - let action: () -> Void - - @EnvironmentObject private var wrapper: NamespaceWrapper - - var body: some View { - let maxIndex = count - 1 - if let size = currency.coinSize(value) { - let offset = size / 16 - ZStack { - ForEach(0...maxIndex, id: \.self) { index in - let match = tappedVal == value && index == maxIndex - let isFlying = flying > 0 - let matchNotFlying = match && !isFlying - let isTarget = matchNotFlying // && isTop - let targetId = isTarget ? String(value) - : String(value) + "+" + String(index) - let yOffset = offset * CGFloat(index) - let xOffset = yOffset / 2 - let shakeOffset: CGFloat = shake && !match ? .random(in: 5...10) - : .zero - let direction = value < tappedVal - OIMcurrencyButton(stack: stack.push(), - value: value, - currency: currency, - availableVal: value, - canEdit: canEdit, -// isFlipped: false, // TODO: Flip coin - pct: match ? 0.0 : 1.0, - action: action) - .offset(x: xOffset + (direction ? shakeOffset : -shakeOffset), - y: yOffset) - .matchedGeometryEffect(id: targetId, in: wrapper.namespace, isSource: false) - .onAppear { -// print("start flying \(id), shaking \(shakeOffset)") - withAnimation(.fly1) { - flying = value // start flying - } - } - } - } - .padding(.trailing, offset * CGFloat(maxIndex)) - .padding(.bottom, offset * CGFloat(maxIndex)) - } - } -} - -// MARK: - -// renders a stack of (identical) coins with offset size/16 -struct OIMcoinStackV2: View { - let stack: CallStack - let value: UInt64 - let count: Int - let tappedVal: UInt64 - @Binding var flying: UInt64 - let canEdit: Bool - let action: () -> Void - - @EnvironmentObject private var cash: OIMcash - @EnvironmentObject private var wrapper: NamespaceWrapper - - var body: some View { - let isFlying = flying > 0 - let match = tappedVal == value - let matchNotFlying = match && !isFlying - let maxIndex = count - 1 - let currency = cash.currency - if let size = currency.coinSize(value) { - let offset = size / 16 - ZStack { - ForEach(0...maxIndex, id: \.self) { index in - let isTop = index == maxIndex - let isTarget = matchNotFlying && isTop - let targetId = isTarget ? String(value) - : String(index) -// if value == 200 || value == 100 { -// let _ = print("OIMcoinStackV", flying, targetId) -// } - let yOffset = offset * CGFloat(index) - let xOffset = yOffset / 2 - let shakeOffset: CGFloat = (cash.shake != 0) && !match ? .random(in: 5...10) - : .zero - let direction = value < tappedVal - OIMcurrencyButton(stack: stack.push(), - value: value, - currency: cash.currency, - availableVal: value, - canEdit: canEdit, -// isFlipped: false, // TODO: Flip coin - pct: match ? 0.0 : 1.0, - action: action) - .offset(x: xOffset + (direction ? shakeOffset : -shakeOffset), - y: yOffset) - .matchedGeometryEffect(id: targetId, in: wrapper.namespace, isSource: false) - .onAppear { - if isTarget { - print(" OIMcoinStackV start flying", flying, targetId) - withAnimation(.fly1) { - flying = value // start flying - } - print(" OIMcoinStackV started flying", flying, targetId) - } - } - } - } - .padding(.trailing, offset * CGFloat(maxIndex)) - .padding(.bottom, offset * CGFloat(maxIndex)) - } - } -} -// MARK: - -//#Preview { -// OIMstackV() -//} diff --git a/TalerWallet1/Views/OIM/OIMcoinsView.swift b/TalerWallet1/Views/OIM/OIMcoinsView.swift @@ -1,86 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI -import taler_swift - -// MARK: - -// renders a spread of coin-stacks in 1 row -struct OIMcoinsView1: View { - let stack: CallStack - let spread: OIMdenominations - let currency: OIMcurrency - @Binding var amountVal: UInt64 - let tappedVal: UInt64 - @Binding var flying: UInt64 - let shake: Bool - let canEdit: Bool - - var body: some View { - let nrOfCoins = currency.bankCoins.count - 1 - HStack(alignment: .top, spacing: 10) { - ForEach(0...nrOfCoins, id: \.self) { index in - let value = currency.bankCoins[index] - let shouldFly = tappedVal == value - let count = spread[index] + (shouldFly ? 1 : 0) - if count > 0 { - OIMcoinStackV1(stack: stack.push(), - value: value, - count: Int(count), - currency: currency, - tappedVal: tappedVal, - flying: $flying, - shake: shake, - canEdit: canEdit - ) { - withAnimation(Animation.easeIn1) { - amountVal -= value - } } } - } // ForEach - } // HStack - } -} -// MARK: - -// renders a spread of coin-stacks in 1 row -struct OIMcoinsView2: View { - let stack: CallStack - let spread: OIMdenominations - @Binding var amountVal: UInt64 - let tappedVal: UInt64 - @Binding var flying: UInt64 - let canEdit: Bool - - @EnvironmentObject private var cash: OIMcash - - var body: some View { - let _ = Self._printChanges() - let currency = cash.currency - let nrOfCoins = currency.bankCoins.count - 1 - HStack(alignment: .top, spacing: 10) { - ForEach(0...nrOfCoins, id: \.self) { index in - let value = currency.bankCoins[index] - let shouldFly = tappedVal == value - let count = spread[index] + (shouldFly ? 1 : 0) - if count > 0 { - OIMcoinStackV2(stack: stack.push(), - value: value, - count: Int(count), - tappedVal: tappedVal, - flying: $flying, - canEdit: canEdit - ) { - withAnimation(Animation.easeIn1) { - amountVal -= value - } } } - } // ForEach - } // HStack - } -} -// MARK: - -//#Preview { -// OIMcoinsView() -//} diff --git a/TalerWallet1/Views/OIM/OIMcurrencyButton.swift b/TalerWallet1/Views/OIM/OIMcurrencyButton.swift @@ -48,7 +48,7 @@ struct OIMmod: AnimatableModifier { let name: String? let availableVal: UInt64 let canEdit: Bool -// let isFlipped: Bool + let isFlipped: Bool var pct: CGFloat let action: () -> Void @@ -63,6 +63,8 @@ struct OIMmod: AnimatableModifier { let image = currencyImage.image .resizable() .scaledToFit() + let oWidth = currencyImage.oWidth / 4 + let oHeight = currencyImage.oHeight / 4 return Group { if canEdit && availableVal >= value { Button(action: action) { @@ -74,10 +76,11 @@ struct OIMmod: AnimatableModifier { .opacity(canEdit ? 0.5 : 1.0) } } - .frame(width: currencyImage.oWidth / 4, height: currencyImage.oHeight / 4) + .frame(width: (isFlipped ? 0.7 * oWidth : oWidth), + height: (isFlipped ? 0.7 * oHeight : oHeight)) .shadow(radius: shadow) -// .rotation3DEffect(.degrees(isFlipped ? 180 : 0), axis: (x: 0, y: 1, z: 0)) -// .animation(.basic1, value: isFlipped) + .rotation3DEffect(.degrees(isFlipped ? 135 : 0), axis: (x: 0, y: 1, z: 0)) + .animation(.basic1, value: isFlipped) .accessibilityLabel(Text("\(value)", comment: "VoiceOver")) // TODO: currency name } } @@ -85,11 +88,10 @@ struct OIMmod: AnimatableModifier { /// renders 1 denomination from a currency struct OIMcurrencyButton: View { let stack: CallStack - let value: UInt64 + let fund: OIMfund let currency: OIMcurrency let availableVal: UInt64 let canEdit: Bool -// let isFlipped: Bool var pct: CGFloat let action: () -> Void @@ -97,6 +99,7 @@ struct OIMcurrencyButton: View { var body: some View { let currency = cash.currency + let value = fund.value let name = value > currency.bankCoins[0] ? currency.noteName(value) : currency.coinName(value) // Use EmptyView, because the modifier actually ignores @@ -105,7 +108,7 @@ struct OIMcurrencyButton: View { name: name, availableVal: availableVal, canEdit: canEdit, -// isFlipped: isFlipped, + isFlipped: fund.state == .flipped, pct: pct, action: action)) } diff --git a/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift b/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift @@ -22,36 +22,33 @@ 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 sourceID = -Int(value) OIMcurrencyButton(stack: stack.push(), - value: value, + fund: fund, currency: currency, availableVal: availableVal, canEdit: true, -// isFlipped: false, pct: 0.0, action: { tappedVal = value } ) -// .matchedGeometryEffect(id: value, in: wrapper.namespace, isSource: true) - .matchedGeometryEffect(id: String(value), in: wrapper.namespace, isSource: true) + .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 sourceID = -Int(value) OIMcurrencyButton(stack: stack.push(), - value: value, + fund: fund, currency: currency, availableVal: availableVal, canEdit: true, -// isFlipped: false, pct: 0.0, action: { tappedVal = value } ) -// .matchedGeometryEffect(id: value, in: wrapper.namespace, isSource: true) - .matchedGeometryEffect(id: String(value), in: wrapper.namespace, isSource: true) + .matchedGeometryEffect(id: String(sourceID), in: wrapper.namespace, isSource: true) } }.padding(.trailing, UIScreen.hasNotch ? UIScreen.horzInsets : 0) // ensure scrolling over the FaceID notch } - .clipped(antialiased: true) - .padding(.horizontal, 5) - .ignoresSafeArea(edges: .horizontal) .viewExtractor { view in if let scrollView = view as? UIScrollView { if #available(iOS 17.4, *) { diff --git a/TalerWallet1/Views/OIM/OIMcurrencyViews2.swift b/TalerWallet1/Views/OIM/OIMcurrencyViews2.swift @@ -1,71 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI -import taler_swift - - -// MARK: - -// renders a stack of (identical) banknotes with offset 10,20 -struct OIMnoteStackV: View { - let stack: CallStack - let value: UInt64 - let count: Int - let currency: OIMcurrency - let tappedVal: UInt64 - @Binding var flying: UInt64 - let shake: Bool - let canEdit: Bool - let action: () -> Void - - @EnvironmentObject private var wrapper: NamespaceWrapper - - var body: some View { -// let _ = Self._printChanges() - let maxIndex = count - 1 - ZStack { - ForEach(0...maxIndex, id: \.self) { index in - let match = tappedVal == value && index == maxIndex - let isFlying = flying > 0 - let matchNotFlying = match && !isFlying - let isTarget = matchNotFlying // && isTop - let targetId = isTarget ? String(value) - : String(value) + "+" + String(index) - let xOffset = CGFloat(10 * index) - let yOffset = CGFloat(20 * index) - let shakeOffset: CGFloat = shake && !match ? .random(in: 10...25) - : .zero - let direction = value < tappedVal -// let _ = print("id \(id), flying \(flying), shaking \(shakeOffset)") - OIMcurrencyButton(stack: stack.push(), - value: value, - currency: currency, - availableVal: value, - canEdit: canEdit, -// isFlipped: false, // TODO: Flip coin - pct: match ? 0.0 : 1.0, - action: action) - .offset(x: xOffset + (direction ? shakeOffset : -shakeOffset), - y: yOffset) - .matchedGeometryEffect(id: targetId, in: wrapper.namespace, isSource: false) - .onAppear { -// print("start flying \(id), shaking \(shakeOffset)") - withAnimation(.fly1) { - flying = value // start flying - } - } - } - } - .padding(.trailing, CGFloat(10 * maxIndex)) - .padding(.bottom, CGFloat(20 * maxIndex)) - } -} - -// MARK: - -//#Preview { -// OIMstackV() -//} diff --git a/TalerWallet1/Views/OIM/OIMlayout.swift b/TalerWallet1/Views/OIM/OIMlayout.swift @@ -18,7 +18,7 @@ struct OIMfundState: LayoutValueKey { static let defaultValue: FundState = .idle } -@available(iOS 16.0, *) +@available(iOS 16.4, *) extension View { func oimID(_ value: Int) -> some View { self.layoutValue(key: OIMid.self, value: value) @@ -31,7 +31,7 @@ extension View { } } -@available(iOS 16.0, *) +@available(iOS 16.4, *) extension LayoutSubview { var oimID: Int { self[OIMid.self] } var oimValue: UInt64 { self[OIMvalue.self] } @@ -40,7 +40,7 @@ extension LayoutSubview { // MARK: - // renders a stack of (identical) banknotes with offset 10,20 -@available(iOS 16.0, *) +@available(iOS 16.4, *) struct OIMlayoutView: View { private let symLog = SymLogV(0) let stack: CallStack @@ -78,7 +78,7 @@ struct OIMlayoutView: View { cash.updateFund(fund) } DispatchQueue.main.async { - withAnimation(.remove1) { + withAnimation(.fly1) { symLog.log(" OIMlayoutView remove \(value)") cash.removeCash(id: fund.id, value: fund.value) } @@ -111,7 +111,7 @@ struct OIMlayoutView: View { } } // MARK: - -@available(iOS 16.0, *) +@available(iOS 16.4, *) struct OIMlayout: Layout { struct CacheData { var maxHeight: CGFloat diff --git a/TalerWallet1/Views/OIM/OIMlineView.swift b/TalerWallet1/Views/OIM/OIMlineView.swift @@ -0,0 +1,156 @@ +/* + * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. + * See LICENSE.md + */ +/** + * @author Marc Stibane + */ +import SwiftUI +import taler_swift + +fileprivate let horzSpacing: CGFloat = 20 +fileprivate let vertSpacing: CGFloat = 10 + +struct OIMlineView: View { + let stack: CallStack + @Binding var amountVal: UInt64 + @Binding var tappedVal: UInt64 + let canEdit: Bool + + @EnvironmentObject private var cash: OIMcash + @EnvironmentObject private var wrapper: NamespaceWrapper + @AppStorage("oimTwoRows") var oimTwoRows: Bool = false + + @State private var flying: UInt64 = 0 // needed for iOS 15 + @State private var myTappedVal: UInt64 = 0 + + var body: some View { +// let _ = Self._printChanges() +#if DEBUG + let debug = 1==0 + let red = debug ? Color.red : Color.clear + let green = debug ? Color.green : Color.clear + let blue = debug ? Color.blue : Color.clear + let orange = debug ? Color.orange : Color.clear +#endif + let currency = cash.currency + + Group { + if #available(iOS 16.4, *) { + let notes = OIMlayoutView(stack: stack.push(), + funds: cash.notes(), + amountVal: $amountVal, + canEdit: true) + let coins = OIMlayoutView(stack: stack.push(), + funds: cash.coins(), + amountVal: $amountVal, + canEdit: true) + LayoutThatFits([HStackLayout(alignment: .center), VStackLayout()]) { + notes +#if DEBUG + .padding(1) + .border(green) +#endif + coins + } +#if DEBUG + .padding(1) + .border(orange) +#endif + } else { // iOS 15 + let result = currency.notesCoins(amountVal) + // Text("notes: \(result.0), coins: \(result.1)") + let notes = OIMnotesView1(stack: stack.push(), + spread: result.0, + currency: currency, + amountVal: $amountVal, + tappedVal: myTappedVal, + flying: $flying, + canEdit: canEdit + ).id("notes") + .matchedGeometryEffect(id: "notes", in: wrapper.namespace) +#if DEBUG + .border(blue) +#endif + let coins = OIMcoinsView1(stack: stack.push(), + spread: result.1, + currency: currency, + amountVal: $amountVal, + tappedVal: myTappedVal, + flying: $flying, + canEdit: canEdit + ).id("coins") + .matchedGeometryEffect(id: "coins", in: wrapper.namespace) +#if DEBUG + .border(red) +#endif + + ScrollView(.horizontal) { + if oimTwoRows { + VStack(spacing: vertSpacing) { + notes + coins + } +#if DEBUG + .padding(1) + .border(green) +#endif + } else { + HStack(alignment: .center, spacing: horzSpacing) { + notes + coins + } +#if DEBUG + .padding(1) + .border(green) +#endif + } + } +#if DEBUG + .padding(1) + .border(orange) +#endif + .clipped() + } // iOS 15 + } + .onChange(of: tappedVal) { newVal in + print(">>tapped ", newVal) + if newVal > 0 { + tappedVal = 0 + let ms = debugAnimations ? 1250 : 250 + // next cycle + DispatchQueue.main.async { + withAnimation(.fly1) { + if #available(iOS 16.4, *) { + print("\n>>addCash", newVal) + cash.addCash(newVal) + amountVal += newVal // update directly + } else { + print("\n>>start flying", newVal, flying) + 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) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) { + print("\n>>dissolve", newVal, flying) + withAnimation(.basic1) { + amountVal += newVal + myTappedVal = 0 + } + } + } + } + } + } + } +} + +// MARK: - +//#Preview { +// OIMlineView() +//} diff --git a/TalerWallet1/Views/OIM/OIMlineViews.swift b/TalerWallet1/Views/OIM/OIMlineViews.swift @@ -1,199 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. - * See LICENSE.md - */ -/** - * @author Marc Stibane - */ -import SwiftUI -import taler_swift - -// renders a spread of banknote-stacks in 1 row -struct OIMnotesView1: View { - let stack: CallStack - let spread: OIMdenominations - let currency: OIMcurrency - @Binding var amountVal: UInt64 - let tappedVal: UInt64 - @Binding var flying: UInt64 - let shake: Bool - let canEdit: Bool - - var body: some View { -// let _ = Self._printChanges() - let nrOfNotes = currency.bankNotes.count - 1 - HStack(alignment: .center, spacing: 10) { - ForEach(0...nrOfNotes, id: \.self) { index in - let value = currency.bankNotes[index] - let shouldFly = tappedVal == value - let count = spread[index] + (shouldFly ? 1 : 0) - if count > 0 { - OIMnoteStackV(stack: stack.push(), - value: value, - count: Int(count), - currency: currency, - tappedVal: tappedVal, - flying: $flying, - shake: shake, - canEdit: canEdit - ) { - withAnimation(.basic1) { - amountVal -= value // remove on button press - } } } - } // ForEach - } // HStack - } -} - -// MARK: - -fileprivate let horzSpacing: CGFloat = 20 -fileprivate let vertSpacing: CGFloat = 10 - -struct OIMlineViews: View { - let stack: CallStack - @Binding var amountVal: UInt64 - @Binding var tappedVal: UInt64 - let canEdit: Bool - - @EnvironmentObject private var cash: OIMcash - @EnvironmentObject private var wrapper: NamespaceWrapper - @AppStorage("oimTwoRows") var oimTwoRows: Bool = false - - @State private var flying: UInt64 = 0 - @State private var myTappedVal: UInt64 = 0 - - var body: some View { -// let _ = Self._printChanges() -#if DEBUG - let debug = 1==0 - let red = debug ? Color.red : Color.clear - let green = debug ? Color.green : Color.clear - let blue = debug ? Color.blue : Color.clear - let orange = debug ? Color.orange : Color.clear -#endif - let currency = cash.currency - let result = currency.notesCoins(amountVal) - // Text("notes: \(result.0), coins: \(result.1)") - let notes = OIMnotesView1(stack: stack.push(), - spread: result.0, - currency: currency, - amountVal: $amountVal, - tappedVal: myTappedVal, - flying: $flying, - shake: (cash.shake != 0), - canEdit: canEdit - ).id("notes") - .matchedGeometryEffect(id: "notes", in: wrapper.namespace) -#if DEBUG - .border(blue) -#endif - let coins = OIMcoinsView1(stack: stack.push(), - spread: result.1, - currency: currency, - amountVal: $amountVal, - tappedVal: myTappedVal, - flying: $flying, - shake: (cash.shake != 0), - canEdit: canEdit - ).id("coins") - .matchedGeometryEffect(id: "coins", in: wrapper.namespace) -#if DEBUG - .border(red) -#endif - - let oneLine = HStack(alignment: .center, spacing: horzSpacing) { - notes - coins - } - let twoLines = VStack(spacing: vertSpacing) { - notes - coins - } - - let scrollView = ScrollView(.horizontal) { - if oimTwoRows { - twoLines -#if DEBUG - .padding(1) - .border(green) -#endif - } else { - oneLine -#if DEBUG - .padding(1) - .border(green) -#endif - } - } - - Group { - if #available(iOS 16.4, *) { - LayoutThatFits([HStackLayout(alignment: .center), VStackLayout()]) { - notes -#if DEBUG - .padding(1) - .border(green) -#endif - coins - } -#if DEBUG - .padding(1) - .border(orange) -#endif - } else { - scrollView -#if DEBUG - .padding(1) - .border(orange) -#endif - .clipped() - } - } - .onChange(of: tappedVal) { newVal in - print(">>tapped ", newVal) - if newVal > 0 { - tappedVal = 0 - let ms = debugAnimations ? 1250 : 250 - - // next cycle - DispatchQueue.main.async { - print("\n>>start flying", newVal, flying) - let addedValue = (newVal == 200) ? 1000 : newVal - withAnimation(.shake1) { - print("addCash", addedValue) - cash.addCash(addedValue) - myTappedVal = newVal - } - 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>>shake start", newVal, flying) - withAnimation(.shake1) { - cash.shake = newVal - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) { - print("\n>>shake end", newVal, flying) - withAnimation(.shake1) { - cash.shake = 0 - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) { - print("\n>>dissolve", newVal, flying) - withAnimation(.basic1) { - amountVal += newVal - myTappedVal = 0 - } - } - } - } - } - } - } - } -} - -// MARK: - -//#Preview { -// OIMlineView() -//}