taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }