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