taler-ios

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

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 }