ScopePicker.swift (10679B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import taler_swift 10 11 fileprivate func formattedAmount(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> (String, String) { 12 let amount = balance.available 13 return amount.formatted(currencyInfo, isNegative: false, useISO: false) 14 } 15 16 fileprivate func urlOrCurrency(_ balance: Balance) -> String { 17 balance.scopeInfo.url?.trimURL ?? balance.scopeInfo.currency 18 } 19 20 fileprivate func pickerRow(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> (String, String) { 21 let formatted = formattedAmount(balance, currencyInfo) 22 let urlOrCurrency = urlOrCurrency(balance) 23 return (String("\(urlOrCurrency):\t\(formatted.0.nbs)"), 24 String("\(urlOrCurrency): \(formatted.1)")) 25 } 26 27 struct CurrencyPicker: View { 28 let stack: CallStack 29 @Binding var value: Int 30 let exchanges: [DefaultExchange] 31 let action: (Int) -> Void 32 33 @State private var selected = 0 34 35 var body: some View { 36 #if PRINT_CHANGES 37 let _ = Self._printChanges() 38 #endif 39 let count = exchanges.count 40 if (count > 0) { 41 Picker(EMPTYSTRING, selection: $selected) { 42 ForEach(0..<count, id: \.self) { index in 43 let exchange = exchanges[index] 44 Text(exchange.currencySpec.name) 45 .tag(index) 46 } 47 } 48 .talerFont(.title3) 49 .task() { 50 // FIXME: check balance for non-zero and disable-peer-payments 51 withAnimation { selected = value } 52 } 53 .onChange(of: selected) { newValue in 54 action(newValue) 55 } 56 57 } 58 } 59 } 60 struct ScopePicker: View { 61 // private let symLog = SymLogV(0) 62 let stack: CallStack 63 @Binding var value: Int 64 let onlyNonZero: Bool 65 let action: (Int) -> Void 66 67 @EnvironmentObject private var controller: Controller 68 69 @State private var selected = 0 70 71 var body: some View { 72 #if PRINT_CHANGES 73 let _ = Self._printChanges() 74 #endif 75 let count = controller.balances.count 76 if (count > 0) { 77 let balance = controller.balances[selected] 78 let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) 79 let available = balance.available 80 let availableA11y = available.formatted(currencyInfo, isNegative: false, 81 useISO: true, a11yDecSep: ".") 82 let url = balance.scopeInfo.url?.trimURL ?? EMPTYSTRING 83 // let a11yLabel = url + ", " + availableA11y 84 let chooseHint = String(localized: "Choose the payment service:", comment: "a11y") 85 let disabled = (count == 1) 86 87 HStack(alignment: .firstTextBaseline) { 88 Text("via", comment: "ScopePicker") 89 .accessibilityHidden(true) 90 .foregroundColor(disabled ? .secondary : .primary) 91 92 if #available(iOS 16.4, *) { 93 ScopeDropDown(selection: $selected, 94 onlyNonZero: onlyNonZero, 95 disabled: disabled) 96 } else { 97 Picker(EMPTYSTRING, selection: $selected) { 98 ForEach(0..<count, id: \.self) { index in 99 let balance = controller.balances[index] 100 let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) 101 let pickerRow = pickerRow(balance, currencyInfo) 102 Text(pickerRow.0) 103 .accessibilityLabel(pickerRow.1) 104 .tag(index) 105 // .selectionDisabled(balance.available.isZero) needs iOS 17 106 } 107 } 108 .disabled(disabled) 109 .frame(maxWidth: .infinity) 110 // .border(.red) // debugging 111 .pickerStyle(.menu) 112 // .accessibilityLabel(a11yLabel) 113 .labelsHidden() // be sure to use valid labels for a11y 114 } 115 } 116 .talerFont(.picker) 117 // .accessibilityLabel(a11yLabel) 118 .accessibilityHint(disabled ? EMPTYSTRING : chooseHint) 119 .task() { 120 // FIXME: check balance for non-zero and disable-peer-payments 121 withAnimation { selected = value } 122 } 123 .onChange(of: selected) { newValue in 124 action(newValue) 125 } 126 } 127 } 128 } 129 // MARK: - 130 @available(iOS 16.4, *) 131 struct ScopeDropDown: View { 132 @Binding var selection: Int 133 let onlyNonZero: Bool 134 let disabled: Bool 135 136 // var maxItemDisplayed: Int = 3 137 138 @EnvironmentObject private var controller: Controller 139 140 @State private var scrollPosition: Int? 141 @State private var showDropdown = false 142 143 func buttonAction() { 144 withAnimation { 145 showDropdown.toggle() 146 } 147 } 148 149 @ViewBuilder 150 func dropDownRow(_ balance: Balance, _ currencyInfo: CurrencyInfo, _ first: Bool = false) -> some View { 151 let urlOrCurrency = urlOrCurrency(balance) 152 let text = Text(urlOrCurrency) 153 let formatted = formattedAmount(balance, currencyInfo) 154 let amount = Text(formatted.0.nbs) 155 if first { 156 let a11yLabel = String(localized: "via \(urlOrCurrency)", comment: "a11y") 157 text 158 .accessibilityLabel(a11yLabel) 159 } else { 160 let a11yLabel = "\(urlOrCurrency), \(formatted.1)" 161 let hLayout = HStack(alignment: .firstTextBaseline) { 162 text 163 Spacer() 164 amount 165 }.padding(.vertical, 4) 166 let vLayout = VStack(alignment: .leading) { 167 text 168 HStack { 169 Spacer() 170 amount 171 Spacer() 172 } 173 } 174 ViewThatFits(in: .horizontal) { 175 hLayout 176 vLayout 177 } 178 .accessibilityElement(children: .combine) 179 .accessibilityLabel(a11yLabel) 180 } 181 } 182 183 var body: some View { 184 let radius = 8.0 185 VStack { 186 let chevron = Image(systemName: CHEVRON_UP) // 187 let foreColor = disabled ? Color.secondary : Color.primary 188 let backColor = disabled ? WalletColors().backgroundColor : WalletColors().gray4 189 let otherBalances = controller.balances.filter { $0 != controller.balances[selection] } 190 let theList = LazyVStack(spacing: 0) { 191 ForEach(0..<otherBalances.count, id: \.self) { index in 192 let balance = otherBalances[index] 193 let disablePeerPayments = balance.disablePeerPayments ?? false 194 let rowDisabled = disablePeerPayments || (onlyNonZero ? balance.available.isZero : false) 195 Button(action: { 196 withAnimation { 197 showDropdown.toggle() 198 selection = controller.balances.firstIndex(of: balance) ?? selection 199 } 200 }, label: { 201 let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) 202 HStack(alignment: .top) { 203 dropDownRow(balance, currencyInfo) // .border(.random) 204 .foregroundColor(rowDisabled ? .secondary : .primary) 205 Spacer() 206 chevron.foregroundColor(.clear) 207 .accessibilityHidden(true) 208 } 209 }) 210 .disabled(rowDisabled) 211 .padding(.horizontal, radius / 2) 212 .frame(maxWidth: .infinity, alignment: .leading) 213 } 214 } 215 VStack { 216 // selected item 217 let balance = controller.balances[selection] 218 let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) 219 let noAmount = !showDropdown 220 Group { 221 if disabled { 222 dropDownRow(balance, currencyInfo, noAmount) 223 } else { 224 Button(action: buttonAction) { 225 HStack(alignment: .firstTextBaseline) { 226 dropDownRow(balance, currencyInfo, noAmount) 227 .accessibilityAddTraits(.isSelected) 228 Spacer() 229 chevron.rotationEffect(.degrees((showDropdown ? -180 : 0))) 230 .accessibilityHidden(true) 231 } 232 } 233 } 234 } 235 .padding(.vertical, 4) 236 .padding(.horizontal, radius / 2) 237 .frame(maxWidth: .infinity, alignment: .leading) 238 // .border(.red) 239 if (showDropdown) { 240 if #available(iOS 17.0, *) { 241 // let toomany = controller.balances.count > maxItemDisplayed 242 // let scrollViewHeight = buttonHeight * CGFloat(toomany ? maxItemDisplayed 243 // : controller.balances.count) 244 ScrollView { 245 theList 246 .scrollTargetLayout() 247 } 248 // .border(.red) 249 .scrollPosition(id: $scrollPosition) 250 .scrollDisabled(controller.balances.count <= 3) 251 // .frame(height: scrollViewHeight) 252 .onAppear { 253 scrollPosition = selection 254 } 255 } else { 256 // Fallback on earlier versions 257 ScrollView { 258 theList 259 } 260 } 261 262 } 263 } 264 .foregroundStyle(foreColor) 265 .background(RoundedRectangle(cornerRadius: radius).fill(backColor)) 266 } 267 .frame(maxWidth: .infinity, alignment: .top) 268 .zIndex(100) 269 } 270 }