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