taler-ios

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

commit f088dfa969c8d868efccb451d36fc9e4b9de2165
parent 8557950065ca8a3aec98365dedf6d7987fb26f8c
Author: Marc Stibane <marc@taler.net>
Date:   Sun, 16 Mar 2025 19:27:52 +0100

cleanup & split OIM

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 24++++++++++++++++++++++--
DTalerWallet1/Views/Balances/OIMView.swift | 484-------------------------------------------------------------------------------
ATalerWallet1/Views/OIM/OIMView.swift | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
RTalerWallet1/Helper/OIMcurrency.swift -> TalerWallet1/Views/OIM/OIMcurrency.swift | 0
ATalerWallet1/Views/OIM/OIMcurrencyViews.swift | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/OIM/OIMlineViews.swift | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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: &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) - } - } - } - 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 - } - 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: &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) + } + } + } + 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 + } + 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() +//}