taler-ios

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

commit cacf407b67c6adb903c2e9e57d3037232da51a13
parent 58e1887689022a0bf836402716fb118bccabce97
Author: Marc Stibane <marc@taler.net>
Date:   Sun, 30 Mar 2025 22:37:31 +0200

prepare OIM

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 6++++++
MTalerWallet1/Views/Balances/BalancesListView.swift | 11-----------
MTalerWallet1/Views/OIM/OIMView.swift | 448++++++++++++++++++++++++++++++++++++++++---------------------------------------
ATalerWallet1/Views/OIM/OIMcurrencyScroller.swift | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/OIM/OIMcurrencyViews.swift | 179++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
MTalerWallet1/Views/OIM/OIMlineViews.swift | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
MTalerWallet1/Views/Sheets/Payment/PaymentView.swift | 8--------
7 files changed, 591 insertions(+), 305 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -198,6 +198,8 @@ 4E847B802C9030E0003A164E /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E847B7E2C9030E0003A164E /* TabBarView.swift */; }; 4E847B822C9065FD003A164E /* ScopePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E847B812C9065FD003A164E /* ScopePicker.swift */; }; 4E847B832C9065FD003A164E /* ScopePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E847B812C9065FD003A164E /* ScopePicker.swift */; }; + 4E84D7AB2D902DE000D2B1CB /* OIMcurrencyScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E84D7AA2D902DE000D2B1CB /* OIMcurrencyScroller.swift */; }; + 4E84D7AC2D902DE000D2B1CB /* OIMcurrencyScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E84D7AA2D902DE000D2B1CB /* OIMcurrencyScroller.swift */; }; 4E84D7B72D96A57900D2B1CB /* LayoutThatFits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E84D7B62D96A57800D2B1CB /* LayoutThatFits.swift */; }; 4E84D7B82D96A57900D2B1CB /* LayoutThatFits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E84D7B62D96A57800D2B1CB /* LayoutThatFits.swift */; }; 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E87C8722A31CB7F001C6406 /* TransactionsEmptyView.swift */; }; @@ -453,6 +455,7 @@ 4E7F85162D63185E00954C30 /* Environment+EdgeInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Environment+EdgeInsets.swift"; sourceTree = "<group>"; }; 4E847B7E2C9030E0003A164E /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = "<group>"; }; 4E847B812C9065FD003A164E /* ScopePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScopePicker.swift; sourceTree = "<group>"; }; + 4E84D7AA2D902DE000D2B1CB /* OIMcurrencyScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OIMcurrencyScroller.swift; sourceTree = "<group>"; }; 4E84D7B62D96A57800D2B1CB /* LayoutThatFits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutThatFits.swift; sourceTree = "<group>"; }; 4E87C8722A31CB7F001C6406 /* TransactionsEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsEmptyView.swift; sourceTree = "<group>"; }; 4E8C171C2A6509BB005B2392 /* Atkinson-Hyperlegible-Regular-102.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Atkinson-Hyperlegible-Regular-102.otf"; sourceTree = "<group>"; }; @@ -1001,6 +1004,7 @@ children = ( 4EE9E1F92D7D516800365E72 /* OIMcurrency.swift */, 4EF97B972D8737250007377E /* OIMcurrencyViews.swift */, + 4E84D7AA2D902DE000D2B1CB /* OIMcurrencyScroller.swift */, 4EF97B9A2D8739630007377E /* OIMlineViews.swift */, 4E47924F2D660C5600749393 /* OIMView.swift */, ); @@ -1406,6 +1410,7 @@ 4E3EAE602A990778009F1BE8 /* P2pReceiveURIView.swift in Sources */, 4E3EAE612A990778009F1BE8 /* ListStyle.swift in Sources */, 4EED38552D140C1400F6C038 /* TabBarModel.swift in Sources */, + 4E84D7AB2D902DE000D2B1CB /* OIMcurrencyScroller.swift in Sources */, 4E3EAE622A990778009F1BE8 /* TransactionSummaryV.swift in Sources */, 4E3EAE632A990778009F1BE8 /* WalletCore.swift in Sources */, 4E3EAE642A990778009F1BE8 /* LaunchAnimationView.swift in Sources */, @@ -1557,6 +1562,7 @@ 4E3B4BC12A41E6C200CC88B8 /* P2pReceiveURIView.swift in Sources */, 4E6EDD872A363D8D0031D520 /* ListStyle.swift in Sources */, 4EED38562D140C1400F6C038 /* TabBarModel.swift in Sources */, + 4E84D7AC2D902DE000D2B1CB /* OIMcurrencyScroller.swift in Sources */, 4EB095582989CBFE0043A8A1 /* TransactionSummaryV.swift in Sources */, 4EB095202989CBCB0043A8A1 /* WalletCore.swift in Sources */, 4EB095672989CBFE0043A8A1 /* LaunchAnimationView.swift in Sources */, diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -20,7 +20,6 @@ struct BalancesListView: View { @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - @AppStorage("sierraLeone") var sierraLeone: Bool = false @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var summary = EMPTYSTRING @@ -65,16 +64,6 @@ struct BalancesListView: View { await refresh() } } - .fullScreenCover(isPresented: $controller.oimModeActive) { - let balance = controller.balances.first - let currency = sierraLeone ? OIMleones : OIMeuros -// let currency = OIMdollars - let _ = symLog("❗️OIMView: \(currency.noteBase)") - OIMView(scope: balance?.scopeInfo, - amount: balance?.available, - currency: currency, - canEdit: true) - } } } } diff --git a/TalerWallet1/Views/OIM/OIMView.swift b/TalerWallet1/Views/OIM/OIMView.swift @@ -13,282 +13,290 @@ let DESCENDING = true public let OIMBACKDARK = "tara-meinczinger-G_yCplAsnB4-unsplash" public let OIMBACKLIGHT = "andrey-haimin-VFUTPASjhB8-unsplash" +fileprivate func intValue(_ amount: Amount?) -> Int { + if let amount { + if !amount.isZero { + let value = amount.value * 100 // TODO: currency specs instead of 100 +// print("intValue: \(Int(value)) \(currency.noteBase)") + return Int(value) + } + } + return 0 +} // MARK: - - -// renders all banknotes and coins in 1 horizontal scrollview -struct OIMcurrencyScroller: View { - let currency: OIMcurrency - @Binding var amountVal: Int +struct OIMamountV: View { + let amount: Amount? + let currStr: String var body: some View { - ScrollView(.horizontal) { - HStack(spacing: 10) { - ForEach(currency.bankNotes, id: \.self) { value in - OIMnoteV(value: value, currency: currency) - .onTapGesture { - withAnimation(Animation.easeIn(duration: 0.25)) { - amountVal += value - } - } - } - ForEach(currency.bankCoins, id: \.self) { value in - OIMcoinV(value: value, currency: currency) - .onTapGesture { - withAnimation(Animation.easeIn(duration: 0.25)) { - amountVal += value - } - } - } - } - } - .viewExtractor { view in - if let scrollView = view as? UIScrollView { - if #available(iOS 17.4, *) { - scrollView.bouncesVertically = false - } else { // Fallback on earlier versions - scrollView.bounces = false - } + HStack { + Spacer() + if let amount { + let amountVal = intValue(amount) + AmountV(currStr, cent: UInt64(amountVal), isNegative: nil) + .foregroundColor(WalletColors().attention) // talerColor) + .onTapGesture { + debugAnimations.toggle() + } } - } - .padding(.bottom, 20) - .padding(.horizontal, 8) + }.padding(.horizontal, UIScreen.hasFaceID ? 20 : 4) + .ignoresSafeArea(edges: .all) } } - // MARK: - -// renders a spread of banknote-stacks in 2 rows -struct NotesView2: View { - let spread: OIMdenominations - let currency: OIMcurrency - - func countVal(index: Int) -> (Int, Int)? { - let nrOfNotes = currency.bankNotes.count - 1 - if index < nrOfNotes { - let count = spread[index] - if count > 0 { - let value = currency.bankNotes[index] - return (value, count) - } - } - return nil - } +struct OIMbackground<Content: View>: View { + let amount: Amount? + let currStr: String + var content: () -> Content + @Environment(\.colorScheme) private var colorScheme var body: some View { - HStack(alignment: .top, spacing: 10) { -// let top0: Bool -// if let cv0 = countVal(index: 0) { -// if cv0.1 > 1 { // -// NoteStackV(value: cv0.0, count: cv0.1, currency: currency) -// top0 = false -// } else { -// NoteStackV(value: cv0.0, count: cv0.1, currency: currency) -// top0 = true -// } -// } else { -// top0 = false -// } -// if let cv1 = countVal(index: 1) { -// -// } + let background = colorScheme == .dark ? OIMBACKDARK + : OIMBACKLIGHT + let backImage = Image(background) + .resizable() + let zStack = ZStack(alignment: .top) { + backImage + .ignoresSafeArea(edges: .all) + .frame(maxWidth: .infinity, maxHeight: .infinity) + content() + OIMamountV(amount: amount, currStr: currStr) + } + if #available(iOS 16.4, *) { + zStack + .toolbar(.hidden, for: .navigationBar, .tabBar) + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) + } else { // Fallback on earlier versions + zStack +// .navigationBarTitle("") // must set the title before you can hide the navigation bar + .navigationBarHidden(true) +// .tabBarHidden(true) // unfortunately this call doesn't exist } } } - // MARK: - -struct Column: Hashable { - let topCount: Int - let topVal: Int - let botVal: Int -} - -typealias Columns = [Column] +struct OIMnavBack<Content: View>: View { + let stack: CallStack + let scope: ScopeInfo? + let currStr: String + @Binding var amount: Amount + @Binding var buttonSelected: Bool + var content: () -> Content -struct ColumnView: View { - let column: Column - let currency: OIMcurrency + @Environment(\.dismiss) var dismiss // call dismiss() to pop back var body: some View { - VStack { - if column.topCount > 1 { -// let _ = print("Stack: \(column.topCount) * \(column.topVal) \(currency.noteBase)") - OIMstackV(value: column.topVal, count: column.topCount, currency: currency) - } else { -// let _ = print("Single: \(column.topVal) and \(column.botVal) \(currency.noteBase)") - OIMsingleV(value: column.topVal, currency: currency) - if column.botVal > 0 { - OIMsingleV(value: column.botVal, currency: currency) + OIMbackground(amount: amount, currStr: currStr) { + ZStack { + content() + VStack { + HStack { + BackButton() { dismiss() } + Spacer() + ForwardButton(enabled: !amount.isZero) { + buttonSelected = true + }.padding(.trailing, UIScreen.horzInsets) + } } + .ignoresSafeArea(edges: .horizontal) } } } } +// MARK: - struct OIMView: View { + let stack: CallStack let scope: ScopeInfo? let amount: Amount? - let currency: OIMcurrency // let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic) - let canEdit: Bool + @Binding var qrButtonTapped: Bool - @Environment(\.colorScheme) private var colorScheme - @AppStorage("oimTwoRows") var oimTwoRows: Bool = false - - @State private var banknotes: Columns = [] - @State private var integerCoins: Columns = [] - @State private var fractalCoins: Columns = [] + @AppStorage("sierraLeone") var sierraLeone: Bool = false @State private var amountVal: Int = 0 + @Namespace var namespace + @State private var tappedVal: Int = 0 + @State private var isFlyingToDeck = false + @State private var shake = false + + var body: some View { +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif + let currency = sierraLeone ? OIMleones : OIMeuros - var intValue: Int { - if let amount { - if !amount.isZero { - let value = amount.value * 100 // TODO: currency specs instead of 100 -// print("intValue: \(Int(value)) \(currency.noteBase)") - return Int(value) + let actions = HStack(spacing: 30) { + QRButton(isNavBarItem: false) { + qrButtonTapped = true } + .lineLimit(5) + .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .center)) + + SendRequestV(stack: stack.push(), + sendDisabled: false, + recvDisabled: false) } - return 0 - } - func buildColumns(_ spread: OIMnotesCoins, currency: OIMcurrency) { - var topVal = 0 - var notes: Columns = [] - var intCoins: Columns = [] - var fracCoins: Columns = [] - - func addTopVal(andBot: Int, to columns: inout Columns) { - if topVal > 0 { - columns.append(Column(topCount: 1, topVal: topVal, botVal: andBot)) -// print("add \(topVal) and \(andBot) \(currency.noteBase)") - topVal = 0 - } else if andBot > 0 { - topVal = andBot - } + OIMbackground(amount: amount, currStr: currency.noteBase) { + VStack { + Spacer() + OIMlineView(currency: currency, + amountVal: $amountVal, + namespace: namespace, + tappedVal: tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: shake, + canEdit: false) + Spacer() + actions + }//.border(.red) + }.task { + amountVal = intValue(amount) } + } +} +// MARK: - +struct OIMPayView: View { + let scope: ScopeInfo? + let amount: Amount? +// let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic) - for (index, count) in spread.0.enumerated() { // banknotes - if count > 0 { - let value = currency.bankNotes[index] - if count > 1 { - if DESCENDING { - addTopVal(andBot: 0, to: &notes) - } // else fill bottom later with smaller note - var cnt = count - while cnt > 5 { - notes.append(Column(topCount: 5, topVal: value, botVal: 0)) - cnt -= 5 - } - notes.append(Column(topCount: cnt, topVal: value, botVal: 0)) -// print("notes: \(count) * \(value) \(currency.noteBase)") - } else { - addTopVal(andBot: value, to: &notes) - } - } + @AppStorage("sierraLeone") var sierraLeone: Bool = false + + @State private var amountVal: Int = 0 + @Namespace var namespace + @State private var tappedVal: Int = 0 + @State private var isFlyingToDeck = false + @State private var shake = false + + var body: some View { +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif + let currency = sierraLeone ? OIMleones : OIMeuros + + OIMbackground(amount: amount, currStr: currency.noteBase) { + VStack { + Spacer() + OIMlineView(currency: currency, + amountVal: $amountVal, + namespace: namespace, + tappedVal: tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: shake, + canEdit: true) + Spacer() + }.border(.red) + }.task { + amountVal = intValue(amount) } - addTopVal(andBot: 0, to: &notes) // TODO: comment out to fill bottom later with coins - banknotes = notes - - for (index, count) in spread.1.enumerated() { - if count > 0 { - let value = currency.bankCoins[index] - if value > 99 { // TODO: use currency decimal - if count > 1 { - if DESCENDING { - addTopVal(andBot: 0, to: &intCoins) - } // else fill bottom later with smaller coin - intCoins.append(Column(topCount: count, topVal: value, botVal: 0)) -// print("intCoins: \(count) * \(value) \(currency.coinBase)") - } else { - addTopVal(andBot: value, to: &intCoins) - } - } else { - if topVal > 99 { - addTopVal(andBot: 0, to: &intCoins) // finish last intCoin + + } +} +// MARK: - +struct OIMEditView: View { + let stack: CallStack + let scope: ScopeInfo? + @Binding var amount: Amount + @Binding var available: Amount +// let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic) + @Binding var buttonSelected: Bool + + @AppStorage("sierraLeone") var sierraLeone: Bool = false + + @State private var amountVal: Int = 0 + @State private var availableVal: Int = 0 + + @Namespace var namespace + @State private var tappedVal: Int = 0 + @State private var isFlyingToDeck = false + @State private var shake = false + + var body: some View { +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif + let currency = sierraLeone ? OIMleones : OIMeuros + + OIMnavBack(stack: stack.push(), + scope: scope, + currStr: currency.noteBase, + amount: $amount, + buttonSelected: $buttonSelected + ) { + VStack { + Spacer() + OIMlineView(currency: currency, + amountVal: $amountVal, + namespace: namespace, + tappedVal: tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: shake, + canEdit: true) + .onChange(of: amountVal) { newVal in + let currencyStr = amount.currencyStr + amount = Amount(currency: currencyStr, cent: UInt64(newVal)) + availableVal = intValue(available) - intValue(amount) } - if count > 1 { - if DESCENDING { - addTopVal(andBot: 0, to: &fracCoins) - } // else fill bottom later with smaller coin - fracCoins.append(Column(topCount: count, topVal: value, botVal: 0)) -// print("fracCoins: \(count) * \(value) \(currency.coinBase)") - } else { - addTopVal(andBot: value, to: &fracCoins) + Spacer() + OIMcurrencyScroller(currency: currency, + availableVal: $availableVal, + amountVal: $amountVal, + namespace: namespace, + tappedVal: $tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: $shake) + .onChange(of: available) { newVal in + availableVal = intValue(newVal) - intValue(amount) } - } } + .border(.red) + }.task { + amountVal = intValue(amount) + availableVal = intValue(available) - intValue(amount) } - if topVal > 99 { - addTopVal(andBot: 0, to: &intCoins) // finish last intCoin - } else { - addTopVal(andBot: 0, to: &fracCoins) - } - integerCoins = intCoins - fractalCoins = fracCoins } +} +// MARK: - +struct OIMSubjectView: View { + let stack: CallStack + let scope: ScopeInfo? + @Binding var amount: Amount +// let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic) + @Binding var buttonSelected: Bool + + @AppStorage("sierraLeone") var sierraLeone: Bool = false var body: some View { #if PRINT_CHANGES || true let _ = Self._printChanges() #endif - let background = colorScheme == .dark ? OIMBACKDARK - : OIMBACKLIGHT - let backImage = Image(background) - .resizable() + let currency = sierraLeone ? OIMleones : OIMeuros - let vStack = VStack { - if let amount { + OIMnavBack(stack: stack.push(), + scope: scope, + currStr: currency.noteBase, + amount: $amount, + buttonSelected: $buttonSelected + ) { + VStack { + Spacer() HStack { - Spacer() - AmountV(scope, amount, isNegative: nil) - }.padding() - } - Spacer() - HStack(alignment: .top, spacing: 10) { - if oimTwoRows { - ForEach(banknotes, id: \.self) { column in - ColumnView(column: column, currency: currency) - } - ForEach(integerCoins, id: \.self) { column in - ColumnView(column: column, currency: currency) - } - HStack(alignment: .top, spacing: 10) { - ForEach(fractalCoins, id: \.self) { column in - ColumnView(column: column, currency: currency) - } - } .padding(.leading, 20) - } else { - OIMlineView(currency: currency, amountVal: $amountVal, canEdit: canEdit, oneLine: false) - } - } - .task { - amountVal = intValue -// amount = 14983 + Text("Subject") } + Spacer() + }.border(.red) + }.task { - Spacer() - if canEdit { - OIMcurrencyScroller(currency: currency, amountVal: $amountVal) - .onChange(of: amountVal) { newValue in - // print("new Amount: \(newValue)") - let result = currency.notesCoins(newValue) - buildColumns(result, currency: currency) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(backImage) - .ignoresSafeArea(edges: .all) - if #available(iOS 16.4, *) { - vStack - .scrollBounceBehavior(.basedOnSize, axes: .horizontal) - } else { // Fallback on earlier versions - vStack } } } + + // MARK: - //#Preview { // OIMView() diff --git a/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift b/TalerWallet1/Views/OIM/OIMcurrencyScroller.swift @@ -0,0 +1,82 @@ +/* + * 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 all banknotes and coins in 1 horizontal scrollview +struct OIMcurrencyScroller: View { + let currency: OIMcurrency + @Binding var availableVal: Int + @Binding var amountVal: Int + let namespace: Namespace.ID + @Binding var tappedVal: Int + @Binding var isFlyingToDeck: Bool + @Binding var shake: Bool + + func tap(value: Int, _ delay: Int = 300) { + isFlyingToDeck = false + tappedVal = value + let ms = debugAnimations ? 1500 : delay + print("tapped \(value)") +// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) { +// print("shake start") +// withAnimation(.shake) { +// shake = true +// } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms)) { + print("shake end") + withAnimation(. shake) { + shake = false + amountVal += value + tappedVal = 0 + } + } +// } + } + var body: some View { + ScrollView(.horizontal) { + HStack(spacing: 10) { + ForEach(currency.bankNotes, id: \.self) { value in + OIMnoteV(value: value, + currency: currency, + availableVal: availableVal, + canEdit: true, + pct: 0.0, + action: { tap(value: value) } + ) + .matchedGeometryEffect(id: value, in: namespace, isSource: true) + } + ForEach(currency.bankCoins, id: \.self) { value in + OIMcoinV(value: value, + currency: currency, + availableVal: availableVal, + canEdit: true, + action: { tap(value: value) } + ) + .matchedGeometryEffect(id: value, in: 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, *) { + scrollView.bouncesVertically = false + } else { // Fallback on earlier versions + scrollView.bounces = false + } + } + } + } +} +// MARK: - +//#Preview { +// OIMcurrencyScroller() +//} diff --git a/TalerWallet1/Views/OIM/OIMcurrencyViews.swift b/TalerWallet1/Views/OIM/OIMcurrencyViews.swift @@ -8,20 +8,81 @@ import SwiftUI import taler_swift +struct OIMbuttonStyle: ButtonStyle { + + public func makeBody(configuration: OIMbuttonStyle.Configuration) -> some View { + configuration.label +// .foregroundColor(.white) +// .padding(15) +// .background(RoundedRectangle(cornerRadius: 5).fill(color)) +// .compositingGroup() +// .shadow(color: .black, radius: 3) + .opacity(configuration.isPressed ? 0.5 : 1.0) + .scaleEffect(configuration.isPressed ? 0.9 : 1.0) + } +} + /// renders 1 banknote from a currency with size/4 struct OIMnoteV: View { let value: Int let currency: OIMcurrency + let availableVal: Int + let canEdit: Bool + var pct: CGFloat + let action: () -> Void var body: some View { - if let name = currency.noteName(value) { - Image(name) - .resizable() - .scaledToFit() +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif + // Use EmptyView, because the modifier actually ignores + // the value passed to its body() function. + EmptyView().modifier(OIMmod(value: value, + currency: currency, + availableVal: availableVal, + canEdit: canEdit, + pct: pct, + action: action)) + } +} + +struct OIMmod: AnimatableModifier { + let value: Int + let currency: OIMcurrency + let availableVal: Int + let canEdit: Bool + var pct: CGFloat + let action: () -> Void + + var animatableData: CGFloat { + get { pct } + set { pct = newValue } + } + + func body(content: Content) -> some View { + let shadow = (3 - 8) * pct + 8 + return Group { + if let name = currency.noteName(value) { + let image = Image(name) + .resizable() + .scaledToFit() + Group { + if canEdit && availableVal >= value { + Button(action: action) { + image + } + .buttonStyle(OIMbuttonStyle()) + } else { + image + .opacity(canEdit ? 0.5 : 1.0) + } + } .frame(width: currency.noteWidth / 4, height: currency.noteHeight / 4) + .shadow(radius: shadow) .accessibilityLabel(Text("\(value)", comment: "VoiceOver")) - } else { - EmptyView() + } else { + EmptyView() + } } } } @@ -30,12 +91,26 @@ struct OIMnoteV: View { struct OIMcoinV: View { let value: Int let currency: OIMcurrency + let availableVal: Int + let canEdit: Bool + let action: () -> Void var body: some View { if let name = currency.coinName(value), let size = currency.coinSize(value) { - Image(name) + let image = Image(name) .resizable() .scaledToFit() + Group { + if canEdit && availableVal >= value { + Button(action: action) { + image + } + .buttonStyle(OIMbuttonStyle()) + } else { + image + .opacity(canEdit ? 0.5 : 1.0) + } + } .frame(width: size / 4, height: size / 4) .accessibilityLabel(Text("\(value)", comment: "VoiceOver")) } else { @@ -48,12 +123,16 @@ struct OIMcoinV: View { struct OIMsingleV: View { let value: Int let currency: OIMcurrency + let availableVal: Int + let canEdit: Bool + var pct: CGFloat + let action: () -> Void var body: some View { if value > currency.bankCoins[0] { - OIMnoteV(value: value, currency: currency) + OIMnoteV(value: value, currency: currency, availableVal: availableVal, canEdit: canEdit, pct: pct, action: action) } else { - OIMcoinV(value: value, currency: currency) + OIMcoinV(value: value, currency: currency, availableVal: availableVal, canEdit: canEdit, action: action) } } } @@ -64,18 +143,45 @@ struct OIMnoteStackV: View { let value: Int let count: Int let currency: OIMcurrency + let namespace: Namespace.ID + let tappedVal: Int + @Binding var isFlyingToDeck: Bool + let shake: Bool + let canEdit: Bool + let action: () -> Void var body: some View { - let offset = 20 +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif let maxIndex = count - 1 ZStack { ForEach(0...maxIndex, id: \.self) { index in - OIMnoteV(value: value, currency: currency) - .offset(x: CGFloat(offset * index), y: CGFloat(offset * index)) + let match = tappedVal == value && index == maxIndex + let id = match && !isFlyingToDeck ? value : -value + let xOffset = CGFloat(10 * index) + let yOffset = CGFloat(20 * index) + let shakeOffset: CGSize = shake ? .random(width: 10...40, height: 5...10) + : .zero + let _ = print("id \(id), flying \(isFlyingToDeck), shaking \(shakeOffset)") + OIMnoteV(value: value, + currency: currency, + availableVal: value, + canEdit: canEdit, + pct: match ? 0.0 : 1.0, + action: action) + .offset(x: xOffset + shakeOffset.width, y: yOffset - shakeOffset.height) + .matchedGeometryEffect(id: id, in: namespace, isSource: false) + .onAppear { + print("start flying \(id), shaking \(shakeOffset)") + withAnimation(.fly) { + isFlyingToDeck = true + } + } } } - .padding(.trailing, CGFloat(offset * maxIndex)) - .padding(.bottom, CGFloat(offset * maxIndex)) + .padding(.trailing, CGFloat(10 * maxIndex)) + .padding(.bottom, CGFloat(20 * maxIndex)) } } @@ -84,46 +190,43 @@ struct OIMcoinStackV: View { let value: Int let count: Int let currency: OIMcurrency + let canEdit: Bool + let action: () -> Void var body: some View { let number = count - 1 -// let _ = print("CoinStack: \(count) * \(value) \(currency.coinBase)") if let size = currency.coinSize(value) { let offset = size / 16 ZStack { ForEach(0...number, id: \.self) { index in - OIMcoinV(value: value, currency: currency) + OIMcoinV(value: value, currency: currency, availableVal: value, canEdit: canEdit, action: action) .offset(x: offset * CGFloat(index), y: offset * CGFloat(index)) } } -//#if DEBUG -// .border(Color.red) -//#endif .padding(.trailing, offset * CGFloat(number)) .padding(.bottom, offset * CGFloat(number)) -//#if DEBUG -// .border(Color.green) -//#endif } } } // renders a stack of 1 denomination -struct OIMstackV: View { - let value: Int - let count: Int - let currency: OIMcurrency - - var body: some View { - if value > currency.bankCoins[0] { - OIMnoteStackV(value: value, count: count, currency: currency) - } else { - OIMcoinStackV(value: value, count: count, currency: currency) - } - } -} - - +//struct OIMstackV: View { +// let value: Int +// let count: Int +// let currency: OIMcurrency +// let canEdit: Bool +// var pct: CGFloat +// let action: () -> Void +// +// var body: some View { +// if value > currency.bankCoins[0] { +// OIMnoteStackV(value: value, count: count, currency: currency, canEdit: canEdit, pct: pct, action: action) +// } else { +// OIMcoinStackV(value: value, count: count, currency: currency, canEdit: canEdit, pct: pct, action: action) +// } +// } +//} +// MARK: - //#Preview { -// OIMView() +// OIMstackV() //} diff --git a/TalerWallet1/Views/OIM/OIMlineViews.swift b/TalerWallet1/Views/OIM/OIMlineViews.swift @@ -13,23 +13,37 @@ struct OIMnotesView1: View { let spread: OIMdenominations let currency: OIMcurrency @Binding var amountVal: Int + let namespace: Namespace.ID + let tappedVal: Int + @Binding var isFlyingToDeck: Bool + let shake: Bool let canEdit: Bool var body: some View { +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif let nrOfNotes = currency.bankNotes.count - 1 HStack(alignment: .top, spacing: 10) { ForEach(0...nrOfNotes, id: \.self) { index in let count = spread[index] - if count > 0 { - let value = currency.bankNotes[index] - let noteStack = OIMnoteStackV(value: value, count: count, currency: currency) - if canEdit { - noteStack.onTapGesture { - withAnimation(Animation.easeIn(duration: 0.25)) { - amountVal -= value - } + let value = currency.bankNotes[index] + let flyingVal = tappedVal == value + if count > 0 || flyingVal { + OIMnoteStackV(value: value, + count: count + (flyingVal ? 1 : 0), + currency: currency, + namespace: namespace, + tappedVal: tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: shake, + canEdit: canEdit + ) { + withAnimation(.basic) { + amountVal -= value // remove on button press } - } else { noteStack } + } + .matchedGeometryEffect(id: tappedVal, in: namespace, isSource: false) } } } @@ -42,6 +56,7 @@ struct OIMcoinsView1: View { let spread: OIMdenominations let currency: OIMcurrency @Binding var amountVal: Int + let namespace: Namespace.ID let canEdit: Bool var body: some View { @@ -51,14 +66,14 @@ struct OIMcoinsView1: View { let count = spread[index] if count > 0 { let value = currency.bankCoins[index] - let coinStack = OIMcoinStackV(value: value, count: count, currency: currency) - if canEdit { - coinStack.onTapGesture { - withAnimation(Animation.easeIn(duration: 0.25)) { - amountVal -= value - } + OIMcoinStackV(value: value, + count: count, + currency: currency, + canEdit: canEdit) { + withAnimation(Animation.easeIn(duration: 0.25)) { + amountVal -= value } - } else { coinStack } + } } } } @@ -66,32 +81,123 @@ struct OIMcoinsView1: View { } // MARK: - +fileprivate let horzSpacing: CGFloat = 20 +fileprivate let vertSpacing: CGFloat = 10 struct OIMlineView: View { let currency: OIMcurrency @Binding var amountVal: Int + let namespace: Namespace.ID + let tappedVal: Int + @Binding var isFlyingToDeck: Bool + let shake: Bool let canEdit: Bool - let oneLine: Bool + + @AppStorage("oimTwoRows") var oimTwoRows: Bool = false var body: some View { +#if PRINT_CHANGES || true + let _ = Self._printChanges() +#endif +#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 result = currency.notesCoins(amountVal) // Text("notes: \(result.0), coins: \(result.1)") - let notes = OIMnotesView1(spread: result.0, currency: currency, amountVal: $amountVal, canEdit: canEdit) - let coins = OIMcoinsView1(spread: result.1, currency: currency, amountVal: $amountVal, canEdit: canEdit) - if oneLine { - HStack { - notes.padding(.trailing, 20) - coins + let notes = OIMnotesView1(spread: result.0, + currency: currency, + amountVal: $amountVal, + namespace: namespace, + tappedVal: tappedVal, + isFlyingToDeck: $isFlyingToDeck, + shake: shake, + canEdit: canEdit + ).id("notes") + .matchedGeometryEffect(id: "notes", in: namespace) +#if DEBUG + .border(blue) +#endif + let coins = OIMcoinsView1(spread: result.1, + currency: currency, + amountVal: $amountVal, + namespace: namespace, + canEdit: canEdit + ).id("coins") + .matchedGeometryEffect(id: "coins", in: namespace) +#if DEBUG + .border(red) +#endif + + let oneLine = HStack(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 } - } else { - VStack { - notes - coins + } + if #available(iOS 16.0, *) { + Group { + if false { + scrollView +#if DEBUG + .padding(1) + .border(orange) +#endif + .clipped() + .scrollIndicators(.visible) + .viewExtractor { view in + if let scrollView = view as? UIScrollView { + if #available(iOS 17.4, *) { + scrollView.bouncesVertically = false + } else { // Fallback on earlier versions + scrollView.bounces = false + } + } + } + } else { + LayoutThatFits([HStackLayout(), VStackLayout()]) { + notes +#if DEBUG + .padding(1) + .border(green) +#endif + coins + } +#if DEBUG + .padding(1) + .border(orange) +#endif + } } + } else { + // Fallback on earlier versions + scrollView } } } +// MARK: - //#Preview { -// OIMView() +// OIMlineView() //} diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift @@ -84,7 +84,6 @@ struct PaymentView: View, Sendable { @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - @AppStorage("sierraLeone") var sierraLeone: Bool = false @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) @State var preparePayResult: PreparePayResult? = nil @@ -298,13 +297,6 @@ struct PaymentView: View, Sendable { } symLog.log("Info(for: \(currency)) loaded: \(currencyInfo.name)") } - .overlay{ - if controller.oimSheetActive { - let currency = sierraLeone ? OIMleones : OIMeuros - let _ = print("❗️OIMView: \(currency.noteBase)") - OIMView(scope: firstScope, amount: effective, currency: currency, canEdit: false) - } - } } else { LoadingView(stack: stack.push(), scopeInfo: nil, message: url.host) .task { await viewDidLoad() }