taler-ios

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

commit 6f0d9e24c69adbb5ac1b69021a80829f0eaf81e7
parent 5328414df3b429d8b56f102f4313c9f181c4f1c6
Author: Marc Stibane <marc@taler.net>
Date:   Mon, 14 Oct 2024 23:11:35 +0200

ScopeDropDown

Diffstat:
MTalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift | 58+++++++++++++++++-----------------------------------------
MTalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift | 93+++++++++++++++++++++++++++++++++++--------------------------------------------
MTalerWallet1/Views/HelperViews/ScopePicker.swift | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
3 files changed, 233 insertions(+), 113 deletions(-)

diff --git a/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift @@ -19,44 +19,24 @@ struct RequestPayment: View { @Binding var summary: String @State private var balanceIndex = 0 - @State private var pickerBalances: [Balance] = [] @State private var balance: Balance? = nil // nil only when balances == [] var body: some View { #if PRINT_CHANGES let _ = Self._printChanges() - let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif + let count = balances.count + let _ = symLog.log("count = \(count)") let scrollView = ScrollView { - let count = pickerBalances.count - let _ = symLog.log("count = \(count)") if count > 0 { - let disabled = (count == 1 || selectedBalance != nil) - Group { - let scopePicker = ScopePicker(value: $balanceIndex, balances: pickerBalances) { index in + ScopePicker(value: $balanceIndex, + balances: balances, + onlyNonZero: false) { index in balanceIndex = index - balance = pickerBalances[index] - } - let available = balance?.available - let availableA11y = available?.formatted(isNegative: false, useISO: true, a11y: ".") - - let url = balance?.scopeInfo.url?.trimURL ?? EMPTYSTRING - let a11yLabel = url + ", " + (availableA11y ?? EMPTYSTRING) - HStack { - Text("via", comment: "ScopePicker") - .foregroundColor(disabled ? .secondary : .primary) - Spacer(minLength: 2) - scopePicker - .disabled(disabled) - Spacer(minLength: 2) - } - .accessibilityElement(children: .combine) - .accessibilityHint(String(localized: "Choose the payment provider.", comment: "a11y")) - .accessibilityLabel(a11yLabel) + balance = balances[index] } - .talerFont(.picker) -// .padding(.bottom) - .padding(.leading) + .padding(.horizontal) + .padding(.bottom, 4) } RequestPaymentContent(stack: stack.push(), balance: $balance, @@ -65,27 +45,23 @@ struct RequestPayment: View { summary: $summary) } // ScrollView .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) -// .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 .task { if let selectedBalance { - pickerBalances = [selectedBalance] balance = selectedBalance + balanceIndex = balances.firstIndex(of: selectedBalance) ?? 0 } else { - pickerBalances = balances - let count = pickerBalances.count - if balanceIndex >= count { - balanceIndex = 0 - } - if count > 0 { - balance = pickerBalances[balanceIndex] - } else { - balance = nil - } + balanceIndex = 0 + balance = (count > 0) ? balances[0] : nil } } if #available(iOS 16.0, *) { - scrollView.toolbar(.hidden, for: .tabBar) + if #available(iOS 16.4, *) { + scrollView.toolbar(.hidden, for: .tabBar) + .scrollBounceBehavior(.basedOnSize) + } else { + scrollView.toolbar(.hidden, for: .tabBar) + } } else { scrollView } diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift @@ -14,48 +14,38 @@ struct SendAmountV: View { private let symLog = SymLogV(0) let stack: CallStack let balances: [Balance] - @Binding var selectedBalance: Balance? // selected balance when the action button is tapped in Transactions + @Binding var selectedBalance: Balance? @Binding var amountLastUsed: Amount @Binding var summary: String @State private var balanceIndex = 0 - @State private var nonZeroBalances: [Balance] = [] - @State private var balance: Balance? = nil // nil only when (balances / nonZeroBalances) == [] + @State private var balance: Balance? = nil // nil only when balances == [] + + func firstNonZero() -> Balance? { + for aBalance in balances { + if !aBalance.available.isZero { + return aBalance + } + } + return nil + } var body: some View { #if PRINT_CHANGES let _ = Self._printChanges() #endif + let count = balances.count + let _ = symLog.log("count = \(count)") let scrollView = ScrollView { - let count = nonZeroBalances.count - let _ = symLog.log("count = \(count)") if count > 0 { - let disabled = (count == 1 || selectedBalance != nil) - Group { - let scopePicker = ScopePicker(value: $balanceIndex, balances: nonZeroBalances) { index in - balanceIndex = index - balance = nonZeroBalances[index] - } - let available = balance?.available - let availableA11y = available?.formatted(isNegative: false, useISO: true, a11y: ".") - - let url = balance?.scopeInfo.url?.trimURL ?? EMPTYSTRING - let a11yLabel = url + ", " + (availableA11y ?? EMPTYSTRING) - HStack { - Text("via", comment: "ScopePicker") - .foregroundColor(disabled ? .secondary : .primary) - Spacer(minLength: 2) - scopePicker - .disabled(disabled) - Spacer(minLength: 2) - } - .accessibilityElement(children: .combine) - .accessibilityHint(String(localized: "Choose the payment provider.", comment: "a11y")) - .accessibilityLabel(a11yLabel) + ScopePicker(value: $balanceIndex, + balances: balances, + onlyNonZero: true) { index in + balanceIndex = index + balance = balances[index] } - .talerFont(.picker) -// .padding(.bottom) - .padding(.leading) + .padding(.horizontal) + .padding(.bottom, 4) } SendAmountContent(stack: stack.push(), balance: $balance, @@ -64,27 +54,32 @@ struct SendAmountV: View { summary: $summary) } // ScrollView .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) -// .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 .task { if let selectedBalance { - nonZeroBalances = [selectedBalance] - balance = selectedBalance - } else { - nonZeroBalances = Balance.nonZeroBalances(balances) - let count = nonZeroBalances.count - if balanceIndex >= count { - balanceIndex = 0 - } - if count > 0 { - balance = nonZeroBalances[balanceIndex] + if selectedBalance.available.isZero { + // find another balance + balance = firstNonZero() } else { - balance = nil + balance = selectedBalance } + } else { + balance = firstNonZero() + } + if let balance { + balanceIndex = balances.firstIndex(of: balance) ?? 0 + } else { + balanceIndex = 0 + balance = (count > 0) ? balances[0] : nil } } if #available(iOS 16.0, *) { - scrollView.toolbar(.hidden, for: .tabBar) + if #available(iOS 16.4, *) { + scrollView.toolbar(.hidden, for: .tabBar) + .scrollBounceBehavior(.basedOnSize) + } else { + scrollView.toolbar(.hidden, for: .tabBar) + } } else { scrollView } @@ -92,7 +87,7 @@ struct SendAmountV: View { } // MARK: - struct SendAmountContent: View { - private let symLog = SymLogV() + private let symLog = SymLogV(0) let stack: CallStack @Binding var balance: Balance? @Binding var balanceIndex: Int @@ -229,17 +224,10 @@ struct SendAmountContent: View { summary: $summary, expireDays: $expireDays) Group { - HStack { - Spacer() - Text("available: \(availableStr)") - .accessibilityLabel("available: \(availableA11y)") - }.padding(.horizontal) - let amountLabel = minimalistic ? String(localized: "Amount:") - : String(localized: "Amount to send:") AmountInputV(stack: stack.push(), currencyInfo: $currencyInfo, amountAvailable: $amountAvailable, - amountLabel: amountLabel, + amountLabel: nil, // will use "Available: xxx", trailing amountToTransfer: $amountToTransfer, amountLastUsed: amountLastUsed, wireFee: nil, @@ -281,6 +269,7 @@ struct SendAmountContent: View { symLog.log("❗️ \(navTitle) onDisappear") } .task(id: balanceIndex + (1000 * controller.currencyTicker)) { + symLog.log("❗️ task \(balanceIndex)") if let balance { scopeInfo = balance.scopeInfo let currency = scopeInfo.currency diff --git a/TalerWallet1/Views/HelperViews/ScopePicker.swift b/TalerWallet1/Views/HelperViews/ScopePicker.swift @@ -7,38 +7,71 @@ */ import SwiftUI +fileprivate func formattedAmount(_ balance: Balance) -> String { + let amount = balance.available + return amount.formatted(isNegative: false, useISO: false) +} + +fileprivate func urlOrCurrency(_ balance: Balance) -> String { + balance.scopeInfo.url?.trimURL ?? balance.scopeInfo.currency +} + +fileprivate func pickerRow(_ balance: Balance) -> String { + String("\(urlOrCurrency(balance)):\t\(formattedAmount(balance).nbs)") +} + struct ScopePicker: View { +// private let symLog = SymLogV(0) @Binding var value: Int let balances: [Balance] + let onlyNonZero: Bool let action: (Int) -> Void @State private var selected = 0 - func formattedAmount(_ balance: Balance) -> String { - let amount = balance.available - return amount.formatted(isNegative: false, useISO: false) - } + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() +#endif + let count = balances.count + if (count > 0) { + let balance = balances[selected] + let available = balance.available + let availableA11y = available.formatted(isNegative: false, useISO: true, a11y: ".") + let url = balance.scopeInfo.url?.trimURL ?? EMPTYSTRING + let a11yLabel = url + ", " + availableA11y - func row(_ balance: Balance) -> String { - let urlOrCurrency = balance.scopeInfo.url?.trimURL - ?? balance.scopeInfo.currency - return String("\(urlOrCurrency):\t\(formattedAmount(balance).nbs)") - } + HStack(alignment: .top) { + let disabled = (count == 1) + Text("via", comment: "ScopePicker") + .accessibilityHidden(true) + .foregroundColor(disabled ? .secondary : .primary) - var body: some View { - if (balances.count > 0) { - Group { - Picker(EMPTYSTRING, selection: $selected) { - ForEach(0..<balances.count, id: \.self) { index in - Text(row(balances[index])) - .tag(index) + if #available(iOS 16.0, *) { + ScopeDropDown(selection: $selected, + balances: balances, + onlyNonZero: onlyNonZero, + disabled: disabled) + } else { + Picker(EMPTYSTRING, selection: $selected) { + ForEach(0..<balances.count, id: \.self) { index in + let balance = balances[index] + Text(pickerRow(balance)) + .tag(index) +// .selectionDisabled(balance.available.isZero) needs iOS 17 + } } + .disabled(disabled) + .frame(maxWidth: .infinity) +// .border(.red) // debugging + .pickerStyle(.menu) + .labelsHidden() } - .frame(width: .infinity) - .pickerStyle(.menu) - .labelsHidden() } - .onAppear() { + .talerFont(.picker) + .accessibilityLabel(a11yLabel) + .accessibilityHint(String(localized: "Choose the payment provider.", comment: "a11y")) + .task() { withAnimation { selected = value } } .onChange(of: selected) { newValue in @@ -48,6 +81,128 @@ struct ScopePicker: View { } } // MARK: - +@available(iOS 16.0, *) +struct ScopeDropDown: View { + @Binding var selection: Int + let balances: [Balance] + let onlyNonZero: Bool + let disabled: Bool + +// var maxItemDisplayed: Int = 3 + + @State private var scrollPosition: Int? + @State private var showDropdown = false + + func buttonAction() { + withAnimation { + showDropdown.toggle() + } + } + + @ViewBuilder + func dropDownRow(_ balance: Balance) -> some View { + let hLayout = HStack(alignment: .top) { + Text(urlOrCurrency(balance)) + Spacer() + Text(formattedAmount(balance).nbs) + } + let vLayout = VStack(alignment: .leading) { + Text(urlOrCurrency(balance)) + HStack { + Spacer() + Text(formattedAmount(balance).nbs) + Spacer() + } + } + ViewThatFits(in: .horizontal) { + hLayout + vLayout + } + } + + var body: some View { + let radius = 8.0 + VStack { + let chevron = Image(systemName: "chevron.up") + let foreColor = disabled ? Color.secondary : Color.primary + let backColor = disabled ? WalletColors().backgroundColor : WalletColors().gray4 + let otherBalances = balances.filter { $0 != balances[selection] } + let theList = LazyVStack(spacing: 0) { + ForEach(0..<otherBalances.count, id: \.self) { index in + let item = otherBalances[index] + let rowDisabled = onlyNonZero ? item.available.isZero : false + Button(action: { + withAnimation { + showDropdown.toggle() + selection = balances.firstIndex(of: item) ?? selection + } + }, label: { + HStack(alignment: .top) { + dropDownRow(item) // .border(.random) + .foregroundColor(rowDisabled ? .secondary : .primary) + Spacer() + chevron.foregroundColor(.clear) + .accessibilityHidden(true) + } + + + }) // .accessibilityElement(children: .combine) + .disabled(rowDisabled) + .padding(.horizontal, radius / 2) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + VStack { + // selected item + let balance = balances[selection] + Button(action: buttonAction) { + HStack(alignment: .firstTextBaseline) { + dropDownRow(balance) + .accessibilityAddTraits(.isSelected) + Spacer() + if !disabled { + chevron.rotationEffect(.degrees((showDropdown ? -180 : 0))) + .accessibilityHidden(true) + } + } + } + .padding(.horizontal, radius / 2) + .frame(maxWidth: .infinity, alignment: .leading) +// .border(.red) + if (showDropdown) { + if #available(iOS 17.0, *) { +// let toomany = balances.count > maxItemDisplayed +// let scrollViewHeight = buttonHeight * CGFloat(toomany ? maxItemDisplayed +// : balances.count) + ScrollView { + theList + .scrollTargetLayout() + } +// .border(.red) + .scrollPosition(id: $scrollPosition) + .scrollDisabled(balances.count <= 3) +// .frame(height: scrollViewHeight) + .onAppear { + scrollPosition = selection + } + } else { + // Fallback on earlier versions + ScrollView { + theList + } + } + + } + } + .foregroundStyle(foreColor) + .background(RoundedRectangle(cornerRadius: radius).fill(backColor)) + } + .frame(maxWidth: .infinity, alignment: .top) + .zIndex(100) + } +} + +// MARK: - //#Preview { // ScopePicker() //}