commit f088dfa969c8d868efccb451d36fc9e4b9de2165
parent 8557950065ca8a3aec98365dedf6d7987fb26f8c
Author: Marc Stibane <marc@taler.net>
Date: Sun, 16 Mar 2025 19:27:52 +0100
cleanup & split OIM
Diffstat:
6 files changed, 570 insertions(+), 486 deletions(-)
diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj
@@ -325,6 +325,10 @@
4EED38582D1485AB00F6C038 /* ForEachWithIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED38572D1485AB00F6C038 /* ForEachWithIndex.swift */; };
4EED38592D1485AB00F6C038 /* ForEachWithIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED38572D1485AB00F6C038 /* ForEachWithIndex.swift */; };
4EF840A72A0B85F400EE0D47 /* CopyShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */; };
+ 4EF97B982D8737250007377E /* OIMcurrencyViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF97B972D8737250007377E /* OIMcurrencyViews.swift */; };
+ 4EF97B992D8737250007377E /* OIMcurrencyViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF97B972D8737250007377E /* OIMcurrencyViews.swift */; };
+ 4EF97B9B2D8739630007377E /* OIMlineViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF97B9A2D8739630007377E /* OIMlineViews.swift */; };
+ 4EF97B9C2D8739630007377E /* OIMlineViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF97B9A2D8739630007377E /* OIMlineViews.swift */; };
4EFA39602AA7946B00742548 /* ToSButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFA395F2AA7946B00742548 /* ToSButtonView.swift */; };
4EFA39612AA7946B00742548 /* ToSButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFA395F2AA7946B00742548 /* ToSButtonView.swift */; };
4EFFDD6B2A501121000C1C6A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 4EFFDD6A2A501121000C1C6A /* Localizable.xcstrings */; };
@@ -539,6 +543,8 @@
4EED38542D140C1400F6C038 /* TabBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarModel.swift; sourceTree = "<group>"; };
4EED38572D1485AB00F6C038 /* ForEachWithIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForEachWithIndex.swift; sourceTree = "<group>"; };
4EF840A62A0B85F400EE0D47 /* CopyShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyShare.swift; sourceTree = "<group>"; };
+ 4EF97B972D8737250007377E /* OIMcurrencyViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OIMcurrencyViews.swift; sourceTree = "<group>"; };
+ 4EF97B9A2D8739630007377E /* OIMlineViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OIMlineViews.swift; sourceTree = "<group>"; };
4EFA395F2AA7946B00742548 /* ToSButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToSButtonView.swift; sourceTree = "<group>"; };
4EFFDD6A2A501121000C1C6A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
AB710490285995B6008B04F0 /* taler-swift */ = {isa = PBXFileReference; lastKnownFileType = text; path = "taler-swift"; sourceTree = SOURCE_ROOT; };
@@ -746,7 +752,6 @@
4E16E12229F3BB99008B9C86 /* CurrencySpecification.swift */,
4EAD117529F672FA008EDD0B /* KeyboardResponder.swift */,
4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */,
- 4EE9E1F92D7D516800365E72 /* OIMcurrency.swift */,
4E578E912A481D8600F21F1C /* Controller+playSound.swift */,
4EB095062989CB7C0043A8A1 /* TalerDater.swift */,
4EB095072989CB7C0043A8A1 /* TalerStrings.swift */,
@@ -809,6 +814,7 @@
4EB095412989CBFE0043A8A1 /* Main */,
4EE77E832C1012F7007C9064 /* Actions */,
4EB095342989CBFE0043A8A1 /* Balances */,
+ 4EF97B962D8735E10007377E /* OIM */,
4EB0952E2989CBFE0043A8A1 /* Transactions */,
4EB095242989CBFE0043A8A1 /* Settings */,
4EEC157129F7188B00D46A03 /* Sheets */,
@@ -870,7 +876,6 @@
isa = PBXGroup;
children = (
4EB095372989CBFE0043A8A1 /* BalancesListView.swift */,
- 4E47924F2D660C5600749393 /* OIMView.swift */,
4EB0953A2989CBFE0043A8A1 /* BalancesSectionView.swift */,
4E77976E2C4BEA4E005D6ECB /* BalanceCellV.swift */,
4E448AB62C4A4109007D5C92 /* BalancesPendingRowV.swift */,
@@ -975,6 +980,17 @@
path = Sheets;
sourceTree = "<group>";
};
+ 4EF97B962D8735E10007377E /* OIM */ = {
+ isa = PBXGroup;
+ children = (
+ 4EE9E1F92D7D516800365E72 /* OIMcurrency.swift */,
+ 4EF97B972D8737250007377E /* OIMcurrencyViews.swift */,
+ 4EF97B9A2D8739630007377E /* OIMlineViews.swift */,
+ 4E47924F2D660C5600749393 /* OIMView.swift */,
+ );
+ path = OIM;
+ sourceTree = "<group>";
+ };
4EFDC38E2CBE8D4E00BE8DBC /* Banking */ = {
isa = PBXGroup;
children = (
@@ -1339,6 +1355,7 @@
4E605DBA2AB05FB6002FB9A7 /* BarGraph.swift in Sources */,
4E2B337D2C8B1D5500186A3E /* ActionsSheet.swift in Sources */,
4E3EAE4D2A990778009F1BE8 /* P2pAcceptDone.swift in Sources */,
+ 4EF97B982D8737250007377E /* OIMcurrencyViews.swift in Sources */,
4E3EAE4E2A990778009F1BE8 /* AnyTransition+backslide.swift in Sources */,
4EFA39602AA7946B00742548 /* ToSButtonView.swift in Sources */,
4E3EAE4F2A990778009F1BE8 /* TwoRowButtons.swift in Sources */,
@@ -1377,6 +1394,7 @@
E37AA62A2AF197E5003850CF /* Model+Refund.swift in Sources */,
4E3EAE682A990778009F1BE8 /* WalletModel.swift in Sources */,
4E3EAE692A990778009F1BE8 /* URLSheet.swift in Sources */,
+ 4EF97B9B2D8739630007377E /* OIMlineViews.swift in Sources */,
4E3EAE6A2A990778009F1BE8 /* ThreeAmountsSection.swift in Sources */,
4E3EAE6B2A990778009F1BE8 /* Model+Withdraw.swift in Sources */,
4ED80E882B8F5FB8008BD576 /* CStringArray.swift in Sources */,
@@ -1484,6 +1502,7 @@
4E605DBB2AB05FB6002FB9A7 /* BarGraph.swift in Sources */,
4E2B337E2C8B1D5500186A3E /* ActionsSheet.swift in Sources */,
4E3B4BC32A42252300CC88B8 /* P2pAcceptDone.swift in Sources */,
+ 4EF97B992D8737250007377E /* OIMcurrencyViews.swift in Sources */,
4E363CBE2A23CB2100D7E98C /* AnyTransition+backslide.swift in Sources */,
4EFA39612AA7946B00742548 /* ToSButtonView.swift in Sources */,
4EB065442A4CD1A80039B91D /* TwoRowButtons.swift in Sources */,
@@ -1522,6 +1541,7 @@
E37AA62B2AF197E5003850CF /* Model+Refund.swift in Sources */,
4EB095162989CBB00043A8A1 /* WalletModel.swift in Sources */,
4EB0955A2989CBFE0043A8A1 /* URLSheet.swift in Sources */,
+ 4EF97B9C2D8739630007377E /* OIMlineViews.swift in Sources */,
4ED2F94B2A278F5100453B40 /* ThreeAmountsSection.swift in Sources */,
4EB095622989CBFE0043A8A1 /* Model+Withdraw.swift in Sources */,
4ED80E892B8F5FB8008BD576 /* CStringArray.swift in Sources */,
diff --git a/TalerWallet1/Views/Balances/OIMView.swift b/TalerWallet1/Views/Balances/OIMView.swift
@@ -1,484 +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
-
-let DESCENDING = true
-
-public let OIMBACKDARK = "tara-meinczinger-G_yCplAsnB4-unsplash"
-public let OIMBACKLIGHT = "andrey-haimin-VFUTPASjhB8-unsplash"
-
-// MARK: -
-/// renders 1 banknote from a currency with size/4
-struct OIMnoteV: View {
- let value: Int
- let currency: OIMcurrency
-
- var body: some View {
- if let name = currency.noteName(value) {
- Image(name)
- .resizable()
- .scaledToFit()
- .frame(width: currency.noteWidth / 4, height: currency.noteHeight / 4)
- .accessibilityLabel(Text("\(value)", comment: "VoiceOver"))
- } else {
- EmptyView()
- }
- }
-}
-
-/// renders 1 coin from a currency with size/4
-struct OIMcoinV: View {
- let value: Int
- let currency: OIMcurrency
-
- var body: some View {
- if let name = currency.coinName(value), let size = currency.coinSize(value) {
- Image(name)
- .resizable()
- .scaledToFit()
- .frame(width: size / 4, height: size / 4)
- .accessibilityLabel(Text("\(value)", comment: "VoiceOver"))
- } else {
- EmptyView()
- }
- }
-}
-
-/// renders 1 denomination from a currency
-struct OIMsingleV: View {
- let value: Int
- let currency: OIMcurrency
-
- var body: some View {
- if value > currency.bankCoins[0] {
- OIMnoteV(value: value, currency: currency)
- } else {
- OIMcoinV(value: value, currency: currency)
- }
- }
-}
-
-// MARK: -
-// renders a stack of (identical) banknotes with offset 20
-struct OIMnoteStackV: View {
- let value: Int
- let count: Int
- let currency: OIMcurrency
-
- var body: some View {
- let offset = 20
- 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))
- }
- }
- .padding(.trailing, CGFloat(offset * maxIndex))
- .padding(.bottom, CGFloat(offset * maxIndex))
- }
-}
-
-// renders a stack of (identical) coins with offset size/16
-struct OIMcoinStackV: View {
- let value: Int
- let count: Int
- let currency: OIMcurrency
-
- 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)
- .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)
- }
- }
-}
-
-// MARK: -
-// renders a spread of banknote-stacks in 1 row
-struct OIMnotesView1: View {
- let spread: OIMdenominations
- let currency: OIMcurrency
- @Binding var amount: Int
-
- var body: some View {
- 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]
- OIMnoteStackV(value: value, count: count, currency: currency)
- .onTapGesture {
- withAnimation(Animation.easeIn(duration: 0.25)) {
- amount -= value
- }
- }
- }
- }
- }
- }
-}
-
-// renders a spread of coin-stacks in 1 row
-struct OIMcoinsView1: View {
- let spread: OIMdenominations
- let currency: OIMcurrency
- @Binding var amount: Int
-
- var body: some View {
- let nrOfCoins = currency.bankCoins.count - 1
- HStack(alignment: .top, spacing: 10) {
- ForEach(0...nrOfCoins, id: \.self) { index in
- let count = spread[index]
- if count > 0 {
- let value = currency.bankCoins[index]
- OIMcoinStackV(value: value, count: count, currency: currency)
- .onTapGesture {
- withAnimation(Animation.easeIn(duration: 0.25)) {
- amount -= value
- }
- }
- }
- }
- }
- }
-}
-
-// MARK: -
-
-// renders all banknotes and coins in 1 horizontal scrollview
-struct OIMcurrencyScroller: View {
- let currency: OIMcurrency
- @Binding var amount: Int
-
- 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)) {
- amount += value
- }
- }
- }
- ForEach(currency.bankCoins, id: \.self) { value in
- OIMcoinV(value: value, currency: currency)
- .onTapGesture {
- withAnimation(Animation.easeIn(duration: 0.25)) {
- amount += 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
- }
- }
- }
- .padding(.bottom, 20)
- .padding(.horizontal, 8)
- }
-}
-
-// 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
- }
-
-
- 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) {
-//
-// }
- }
- }
-}
-
-// MARK: -
-struct Column: Hashable {
- let topCount: Int
- let topVal: Int
- let botVal: Int
-}
-
-typealias Columns = [Column]
-
-struct ColumnView: View {
- let column: Column
- let currency: OIMcurrency
-
- 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)
- }
- }
- }
- }
-}
-
-struct OIMView: View {
- let balance: Balance?
- let currency: OIMcurrency
-// let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic)
-
- @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 = []
-
- @State private var amount: Int = 0
-
- var intValue: Int {
- if let balance {
- let amount = balance.available
- 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
- }
-
- 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
- }
- }
-
- 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: ¬es)
- } // 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: ¬es)
- }
- }
- }
- addTopVal(andBot: 0, to: ¬es) // 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
- }
- 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)
- }
- }
- }
- }
- if topVal > 99 {
- addTopVal(andBot: 0, to: &intCoins) // finish last intCoin
- } else {
- addTopVal(andBot: 0, to: &fracCoins)
- }
- integerCoins = intCoins
- fractalCoins = fracCoins
- }
-
- var body: some View {
-#if PRINT_CHANGES || true
- let _ = Self._printChanges()
-#endif
- let background = colorScheme == .dark ? OIMBACKDARK
- : OIMBACKLIGHT
- let backImage = Image(background)
- .resizable()
-
- let vStack = VStack {
- 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 {
- let result = currency.notesCoins(amount)
- // Text("notes: \(result.0), coins: \(result.1)")
- OIMnotesView1(spread: result.0, currency: currency, amount: $amount)
- .padding(.trailing, 20)
- OIMcoinsView1(spread: result.1, currency: currency, amount: $amount)
- }
- }
- .task {
- amount = intValue
-// amount = 14983
- }
-
- Spacer()
- OIMcurrencyScroller(currency: currency, amount: $amount)
- .onChange(of: amount) { 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
- }
-
- }
-}
-
-/// https://stackoverflow.com/questions/57132417/swiftui-scrollview-is-not-able-to-disable-vertical-bounce
-extension View {
- @ViewBuilder
- func viewExtractor(result: @escaping (UIView) -> ()) -> some View {
- self
- .background(ViewExtractorHelper(result: result))
- .compositingGroup()
- }
-}
-
-fileprivate struct ViewExtractorHelper: UIViewRepresentable {
- var result: (UIView) -> ()
-
- func makeUIView(context: Context) -> UIView {
- let view = UIView(frame: .zero)
- view.backgroundColor = .clear
- DispatchQueue.main.async {
- if let superView = view.superview?.superview?.subviews.last?.subviews.first {
- result(superView)
- }
- }
- return view
- }
- func updateUIView(_ uiView: UIView, context: Context) {
-
- }
-}
-
-//#Preview {
-// OIMView()
-//}
diff --git a/TalerWallet1/Views/OIM/OIMView.swift b/TalerWallet1/Views/OIM/OIMView.swift
@@ -0,0 +1,322 @@
+/*
+ * This file is part of GNU Taler, ©2022-25 Taler Systems S.A.
+ * See LICENSE.md
+ */
+/**
+ * @author Marc Stibane
+ */
+import SwiftUI
+import taler_swift
+
+let DESCENDING = true
+
+public let OIMBACKDARK = "tara-meinczinger-G_yCplAsnB4-unsplash"
+public let OIMBACKLIGHT = "andrey-haimin-VFUTPASjhB8-unsplash"
+
+// MARK: -
+
+// renders all banknotes and coins in 1 horizontal scrollview
+struct OIMcurrencyScroller: View {
+ let currency: OIMcurrency
+ @Binding var amountVal: Int
+
+ 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
+ }
+ }
+ }
+ .padding(.bottom, 20)
+ .padding(.horizontal, 8)
+ }
+}
+
+// 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
+ }
+
+
+ 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) {
+//
+// }
+ }
+ }
+}
+
+// MARK: -
+struct Column: Hashable {
+ let topCount: Int
+ let topVal: Int
+ let botVal: Int
+}
+
+typealias Columns = [Column]
+
+struct ColumnView: View {
+ let column: Column
+ let currency: OIMcurrency
+
+ 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)
+ }
+ }
+ }
+ }
+}
+
+struct OIMView: View {
+ let scope: ScopeInfo?
+ let amount: Amount?
+ let currency: OIMcurrency
+// let decimal: Int // 0 for ¥,HUF; 2 for $,€,£; 3 for ﷼,₯ (arabic)
+ let canEdit: 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 = []
+
+ @State private var amountVal: Int = 0
+
+ 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)
+ }
+ }
+ 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
+ }
+ }
+
+ 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: ¬es)
+ } // 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: ¬es)
+ }
+ }
+ }
+ addTopVal(andBot: 0, to: ¬es) // 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
+ }
+ 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)
+ }
+ }
+ }
+ }
+ if topVal > 99 {
+ addTopVal(andBot: 0, to: &intCoins) // finish last intCoin
+ } else {
+ addTopVal(andBot: 0, to: &fracCoins)
+ }
+ integerCoins = intCoins
+ fractalCoins = fracCoins
+ }
+
+ var body: some View {
+#if PRINT_CHANGES || true
+ let _ = Self._printChanges()
+#endif
+ let background = colorScheme == .dark ? OIMBACKDARK
+ : OIMBACKLIGHT
+ let backImage = Image(background)
+ .resizable()
+
+ let vStack = VStack {
+ if let amount {
+ 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
+ }
+
+ 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
+ }
+
+ }
+}
+
+/// https://stackoverflow.com/questions/57132417/swiftui-scrollview-is-not-able-to-disable-vertical-bounce
+extension View {
+ @ViewBuilder
+ func viewExtractor(result: @escaping (UIView) -> ()) -> some View {
+ self
+ .background(ViewExtractorHelper(result: result))
+ .compositingGroup()
+ }
+}
+
+fileprivate struct ViewExtractorHelper: UIViewRepresentable {
+ var result: (UIView) -> ()
+
+ func makeUIView(context: Context) -> UIView {
+ let view = UIView(frame: .zero)
+ view.backgroundColor = .clear
+ DispatchQueue.main.async {
+ if let superView = view.superview?.superview?.subviews.last?.subviews.first {
+ result(superView)
+ }
+ }
+ return view
+ }
+ func updateUIView(_ uiView: UIView, context: Context) {
+
+ }
+}
+
+//#Preview {
+// OIMView()
+//}
diff --git a/TalerWallet1/Helper/OIMcurrency.swift b/TalerWallet1/Views/OIM/OIMcurrency.swift
diff --git a/TalerWallet1/Views/OIM/OIMcurrencyViews.swift b/TalerWallet1/Views/OIM/OIMcurrencyViews.swift
@@ -0,0 +1,129 @@
+/*
+ * 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 1 banknote from a currency with size/4
+struct OIMnoteV: View {
+ let value: Int
+ let currency: OIMcurrency
+
+ var body: some View {
+ if let name = currency.noteName(value) {
+ Image(name)
+ .resizable()
+ .scaledToFit()
+ .frame(width: currency.noteWidth / 4, height: currency.noteHeight / 4)
+ .accessibilityLabel(Text("\(value)", comment: "VoiceOver"))
+ } else {
+ EmptyView()
+ }
+ }
+}
+
+/// renders 1 coin from a currency with size/4
+struct OIMcoinV: View {
+ let value: Int
+ let currency: OIMcurrency
+
+ var body: some View {
+ if let name = currency.coinName(value), let size = currency.coinSize(value) {
+ Image(name)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size / 4, height: size / 4)
+ .accessibilityLabel(Text("\(value)", comment: "VoiceOver"))
+ } else {
+ EmptyView()
+ }
+ }
+}
+
+/// renders 1 denomination from a currency
+struct OIMsingleV: View {
+ let value: Int
+ let currency: OIMcurrency
+
+ var body: some View {
+ if value > currency.bankCoins[0] {
+ OIMnoteV(value: value, currency: currency)
+ } else {
+ OIMcoinV(value: value, currency: currency)
+ }
+ }
+}
+
+// MARK: -
+// renders a stack of (identical) banknotes with offset 20
+struct OIMnoteStackV: View {
+ let value: Int
+ let count: Int
+ let currency: OIMcurrency
+
+ var body: some View {
+ let offset = 20
+ 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))
+ }
+ }
+ .padding(.trailing, CGFloat(offset * maxIndex))
+ .padding(.bottom, CGFloat(offset * maxIndex))
+ }
+}
+
+// renders a stack of (identical) coins with offset size/16
+struct OIMcoinStackV: View {
+ let value: Int
+ let count: Int
+ let currency: OIMcurrency
+
+ 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)
+ .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)
+ }
+ }
+}
+
+
+//#Preview {
+// OIMView()
+//}
diff --git a/TalerWallet1/Views/OIM/OIMlineViews.swift b/TalerWallet1/Views/OIM/OIMlineViews.swift
@@ -0,0 +1,97 @@
+/*
+ * 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 spread: OIMdenominations
+ let currency: OIMcurrency
+ @Binding var amountVal: Int
+ let canEdit: Bool
+
+ var body: some View {
+ 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
+ }
+ }
+ } else { noteStack }
+ }
+ }
+ }
+ }
+}
+
+// MARK: -
+// renders a spread of coin-stacks in 1 row
+struct OIMcoinsView1: View {
+ let spread: OIMdenominations
+ let currency: OIMcurrency
+ @Binding var amountVal: Int
+ 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 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
+ }
+ }
+ } else { coinStack }
+ }
+ }
+ }
+ }
+}
+
+// MARK: -
+
+struct OIMlineView: View {
+ let currency: OIMcurrency
+ @Binding var amountVal: Int
+ let canEdit: Bool
+ let oneLine: Bool
+
+ var body: some View {
+ 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
+ }
+ } else {
+ VStack {
+ notes
+ coins
+ }
+ }
+ }
+}
+
+//#Preview {
+// OIMView()
+//}