CurrencyInputView.swift (10374B)
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 let replaceable = 500 12 fileprivate let shortcutValues = [5000,2500,1000] // TODO: adapt for ¥ 13 14 struct ShortcutButton: View { 15 let scope: ScopeInfo? 16 let currency: String 17 let currencyField: CurrencyField 18 let shortcut: Int 19 let available: Amount? // disable if available < value 20 let action: (Int, CurrencyField) -> Void 21 22 func makeButton(with newShortcut: Int) -> ShortcutButton { 23 ShortcutButton(scope: scope, 24 currency: currency, 25 currencyField: currencyField, 26 shortcut: newShortcut, 27 available: available, 28 action: action) 29 } 30 31 func isDisabled(shortie: Amount) -> Bool { 32 if let available { 33 return available.value < shortie.value 34 } 35 return false 36 } 37 38 var body: some View { 39 #if PRINT_CHANGES 40 let _ = Self._printChanges() 41 // let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 42 #endif 43 let shortie = Amount(currency: currency, cent: UInt64(shortcut)) // TODO: adapt for ¥ 44 let title = shortie.formatted(scope, isNegative: false) 45 let shortcutLabel = String(localized: "Shortcut", comment: "a11y: $50,$25,$10,$5 shortcut buttons") 46 let a11yLabel = "\(shortcutLabel) \(title.1)" 47 Button(action: { action(shortcut, currencyField)} ) { 48 Text(title.0) 49 .lineLimit(1) 50 .talerFont(.callout) 51 } 52 // .frame(maxWidth: .infinity) 53 .disabled(isDisabled(shortie: shortie)) 54 .buttonStyle(.bordered) 55 .accessibilityLabel(a11yLabel) 56 } 57 } 58 // MARK: - 59 struct CurrencyInputView: View { 60 let scope: ScopeInfo? 61 @Binding var amount: Amount // the `value´ 62 let amountLastUsed: Amount 63 let available: Amount? 64 let title: String? 65 let a11yTitle: String 66 let shortcutAction: ((_ amount: Amount) -> Void)? 67 68 @EnvironmentObject private var controller: Controller 69 70 @State private var hasBeenShown = false 71 @State private var useShortcut = 0 72 // `body´ builds a new CurrencyField each time, but the text field is created only 73 // once - keep the handle to it here, where it survives the re-evaluations 74 @State private var fieldHandle = CurrencyFieldHandle() 75 76 @MainActor 77 func action(shortcut: Int, currencyField: CurrencyField) { 78 let shortie = Amount(currency: amount.currencyStr, cent: UInt64(shortcut)) // TODO: adapt for ¥ 79 if let shortcutAction { 80 shortcutAction(shortie) 81 } else { 82 useShortcut = shortcut 83 currencyField.updateText(amount: shortie) 84 amount = shortie 85 currencyField.resignFirstResponder() 86 } 87 } 88 89 @MainActor 90 func shortcut(for value: Int,_ currencyField: CurrencyField) -> ShortcutButton { 91 var shortcut = value 92 if value == replaceable { 93 if !amountLastUsed.isZero { 94 let lastUsedD = amountLastUsed.value 95 let lastUsedI = lround(lastUsedD * 100) 96 if !shortcutValues.contains(lastUsedI) { 97 shortcut = lastUsedI 98 } } } 99 return ShortcutButton(scope: scope, 100 currency: amount.currencyStr, 101 currencyField: currencyField, 102 shortcut: shortcut, 103 available: available, 104 action: action) 105 } 106 107 @MainActor 108 func shortcuts(_ currencyField: CurrencyField, _ currencyInfo: CurrencyInfo) -> [ShortcutButton] { 109 var buttons: [ShortcutButton] = [] 110 if let commonAmounts = currencyInfo.commonAmounts { 111 buttons = commonAmounts.prefix(4).map { amount in 112 shortcut(for: Int(amount.centValue), currencyField) 113 } 114 } else { 115 buttons = shortcutValues.map { value in 116 shortcut(for: value, currencyField) 117 } 118 buttons.append(shortcut(for: replaceable, currencyField)) 119 } 120 return buttons 121 } 122 123 func availableString(_ availableStr: String) -> String { 124 String(localized: "Available for transfer: \(availableStr)") 125 } 126 127 var a11yLabel: String { // format currency for a11y 128 availableString(available?.readableDescription ?? String(localized: "unknown")) 129 } 130 131 func heading() -> (String, String)? { 132 if let title { 133 return (title, title) 134 } 135 if let available { 136 let formatted = available.formatted(scope, isNegative: false) 137 return (availableString(formatted.0), availableString(formatted.1)) 138 } 139 return nil 140 } 141 142 func currencyInfo() -> CurrencyInfo { 143 if let scope { 144 return controller.info(for: scope, controller.currencyTicker) 145 } else { 146 return controller.info2(for: amount.currencyStr, controller.currencyTicker) 147 } 148 } 149 150 var body: some View { 151 #if PRINT_CHANGES 152 let _ = Self._printChanges() 153 // let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 154 #endif 155 let currencyInfo = currencyInfo() 156 let currencyField = CurrencyField(currencyInfo, amount: $amount, handle: fieldHandle) 157 VStack (alignment: .center) { // center shortcut buttons 158 if let heading = heading() { 159 Text(heading.0) 160 .padding(.horizontal, 4) 161 .padding(.top) 162 .frame(maxWidth: .infinity, alignment: title != nil ? .leading : .trailing) 163 .talerFont(.title2) 164 .accessibilityLabel(heading.1) 165 .padding(.bottom, -6) 166 } 167 currencyField 168 .accessibilityLabel(a11yTitle) 169 .frame(maxWidth: .infinity, alignment: .trailing) 170 .foregroundColor(WalletColors().fieldForeground) // text color 171 // .background(WalletColors().fieldBackground) // problem: white corners 172 .talerFont(.title2) 173 .textFieldStyle(.roundedBorder) 174 .onTapGesture { 175 if useShortcut != 0 { 176 amount = Amount.zero(currency: amount.currencyStr) 177 useShortcut = 0 178 } 179 } 180 if #available(iOS 16.4, *) { 181 let shortcuts = shortcuts(currencyField, currencyInfo) 182 ViewThatFits(in: .horizontal) { 183 HStack { 184 ForEach(shortcuts, id: \.shortcut) { 185 $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : []) 186 } 187 } 188 VStack { 189 let count = shortcuts.count 190 let half = count / 2 191 HStack { 192 Spacer() 193 ForEach(0..<half, id: \.self) { index in 194 let thisShortcut = shortcuts[index] 195 thisShortcut 196 .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : []) 197 Spacer() 198 } 199 } 200 HStack { 201 Spacer() 202 ForEach(half..<count, id: \.self) { index in 203 let thisShortcut = shortcuts[index] 204 thisShortcut 205 .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : []) 206 Spacer() 207 } 208 } 209 } 210 VStack { 211 ForEach(shortcuts, id: \.shortcut) { 212 $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : []) 213 } 214 } 215 } 216 .padding(.vertical, 6) 217 } // iOS 16+ only 218 }.onAppear { // make CurrencyField show the keyboard after 0.4 seconds 219 #if OIM 220 let oimModeActive = controller.oimModeActive 221 #else 222 let oimModeActive = false 223 #endif 224 if hasBeenShown { 225 // print("❗️Yikes: CurrencyInputView hasBeenShown") 226 } else if !UIAccessibility.isVoiceOverRunning && !oimModeActive { 227 // print("❗️CurrencyInputView❗️") 228 DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { 229 hasBeenShown = true 230 if !oimModeActive { 231 if !currencyField.becomeFirstResponder() { 232 print("❗️Yikes❗️ cannot becomeFirstResponder") 233 } 234 } 235 } 236 } 237 }.onDisappear { 238 currencyField.resignFirstResponder() 239 hasBeenShown = false 240 } 241 #if OIM 242 .onChange(of: controller.oimModeActive) {_ in 243 currencyField.resignFirstResponder() 244 } 245 #endif 246 } 247 } 248 // MARK: - 249 #if DEBUG 250 //fileprivate struct Previews: PreviewProvider { 251 // @MainActor 252 // struct StateContainer: View { 253 // @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0) 254 // @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170) 255 // @State private var previewL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY) 256 // var body: some View { 257 // CurrencyInputView(amount: $amountToPreview, 258 // scope: <#ScopeInfo#>, 259 // amountLastUsed: amountLastUsed, 260 // available: Amount(currency: LONGCURRENCY, cent: 2000), 261 // title: "Amount to withdraw:", 262 // shortcutAction: nil) 263 // } 264 // } 265 // static var previews: some View { 266 // StateContainer() 267 // } 268 //} 269 #endif