CurrencyInputView.swift (10127B)
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 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 73 @MainActor 74 func action(shortcut: Int, currencyField: CurrencyField) { 75 let shortie = Amount(currency: amount.currencyStr, cent: UInt64(shortcut)) // TODO: adapt for ¥ 76 if let shortcutAction { 77 shortcutAction(shortie) 78 } else { 79 useShortcut = shortcut 80 currencyField.updateText(amount: shortie) 81 amount = shortie 82 currencyField.resignFirstResponder() 83 } 84 } 85 86 @MainActor 87 func shortcut(for value: Int,_ currencyField: CurrencyField) -> ShortcutButton { 88 var shortcut = value 89 if value == replaceable { 90 if !amountLastUsed.isZero { 91 let lastUsedD = amountLastUsed.value 92 let lastUsedI = lround(lastUsedD * 100) 93 if !shortcutValues.contains(lastUsedI) { 94 shortcut = lastUsedI 95 } } } 96 return ShortcutButton(scope: scope, 97 currency: amount.currencyStr, 98 currencyField: currencyField, 99 shortcut: shortcut, 100 available: available, 101 action: action) 102 } 103 104 @MainActor 105 func shortcuts(_ currencyField: CurrencyField, _ currencyInfo: CurrencyInfo) -> [ShortcutButton] { 106 var buttons: [ShortcutButton] = [] 107 if let commonAmounts = currencyInfo.commonAmounts { 108 buttons = commonAmounts.prefix(4).map { amount in 109 shortcut(for: Int(amount.centValue), currencyField) 110 } 111 } else { 112 buttons = shortcutValues.map { value in 113 shortcut(for: value, currencyField) 114 } 115 buttons.append(shortcut(for: replaceable, currencyField)) 116 } 117 return buttons 118 } 119 120 func availableString(_ availableStr: String) -> String { 121 String(localized: "Available for transfer: \(availableStr)") 122 } 123 124 var a11yLabel: String { // format currency for a11y 125 availableString(available?.readableDescription ?? String(localized: "unknown")) 126 } 127 128 func heading() -> (String, String)? { 129 if let title { 130 return (title, title) 131 } 132 if let available { 133 let formatted = available.formatted(scope, isNegative: false) 134 return (availableString(formatted.0), availableString(formatted.1)) 135 } 136 return nil 137 } 138 139 func currencyInfo() -> CurrencyInfo { 140 if let scope { 141 return controller.info(for: scope, controller.currencyTicker) 142 } else { 143 return controller.info2(for: amount.currencyStr, controller.currencyTicker) 144 } 145 } 146 147 var body: some View { 148 #if PRINT_CHANGES 149 let _ = Self._printChanges() 150 // let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 151 #endif 152 let currencyInfo = currencyInfo() 153 let currencyField = CurrencyField(currencyInfo, amount: $amount) 154 VStack (alignment: .center) { // center shortcut buttons 155 if let heading = heading() { 156 Text(heading.0) 157 .padding(.horizontal, 4) 158 .padding(.top) 159 .frame(maxWidth: .infinity, alignment: title != nil ? .leading : .trailing) 160 .talerFont(.title2) 161 .accessibilityLabel(heading.1) 162 .padding(.bottom, -6) 163 } 164 currencyField 165 .accessibilityLabel(a11yTitle) 166 .frame(maxWidth: .infinity, alignment: .trailing) 167 .foregroundColor(WalletColors().fieldForeground) // text color 168 // .background(WalletColors().fieldBackground) // problem: white corners 169 .talerFont(.title2) 170 .textFieldStyle(.roundedBorder) 171 .onTapGesture { 172 if useShortcut != 0 { 173 amount = Amount.zero(currency: amount.currencyStr) 174 useShortcut = 0 175 } 176 } 177 if #available(iOS 16.4, *) { 178 let shortcuts = shortcuts(currencyField, currencyInfo) 179 ViewThatFits(in: .horizontal) { 180 HStack { 181 ForEach(shortcuts, id: \.shortcut) { 182 $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : []) 183 } 184 } 185 VStack { 186 let count = shortcuts.count 187 let half = count / 2 188 HStack { 189 Spacer() 190 ForEach(0..<half, id: \.self) { index in 191 let thisShortcut = shortcuts[index] 192 thisShortcut 193 .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : []) 194 Spacer() 195 } 196 } 197 HStack { 198 Spacer() 199 ForEach(half..<count, id: \.self) { index in 200 let thisShortcut = shortcuts[index] 201 thisShortcut 202 .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : []) 203 Spacer() 204 } 205 } 206 } 207 VStack { 208 ForEach(shortcuts, id: \.shortcut) { 209 $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : []) 210 } 211 } 212 } 213 .padding(.vertical, 6) 214 } // iOS 16+ only 215 }.onAppear { // make CurrencyField show the keyboard after 0.4 seconds 216 #if OIM 217 let oimModeActive = controller.oimModeActive 218 #else 219 let oimModeActive = false 220 #endif 221 if hasBeenShown { 222 // print("❗️Yikes: CurrencyInputView hasBeenShown") 223 } else if !UIAccessibility.isVoiceOverRunning && !oimModeActive { 224 // print("❗️CurrencyInputView❗️") 225 DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { 226 hasBeenShown = true 227 if !oimModeActive { 228 if !currencyField.becomeFirstResponder() { 229 print("❗️Yikes❗️ cannot becomeFirstResponder") 230 } 231 } 232 } 233 } 234 }.onDisappear { 235 currencyField.resignFirstResponder() 236 hasBeenShown = false 237 } 238 #if OIM 239 .onChange(of: controller.oimModeActive) {_ in 240 currencyField.resignFirstResponder() 241 } 242 #endif 243 } 244 } 245 // MARK: - 246 #if DEBUG 247 //fileprivate struct Previews: PreviewProvider { 248 // @MainActor 249 // struct StateContainer: View { 250 // @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0) 251 // @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170) 252 // @State private var previewL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY) 253 // var body: some View { 254 // CurrencyInputView(amount: $amountToPreview, 255 // scope: <#ScopeInfo#>, 256 // amountLastUsed: amountLastUsed, 257 // available: Amount(currency: LONGCURRENCY, cent: 2000), 258 // title: "Amount to withdraw:", 259 // shortcutAction: nil) 260 // } 261 // } 262 // static var previews: some View { 263 // StateContainer() 264 // } 265 //} 266 #endif