taler-ios

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

TabBarView.swift (7534B)


      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 SymLog
     10 
     11 struct ActionItem: View {
     12     private let symLog = SymLogV(0)
     13     let stack: CallStack
     14     let tab: TalerTab
     15     let onTap: () -> Void
     16     let onDrag: (CallStack) -> Void
     17 
     18     @EnvironmentObject private var controller: Controller
     19     @AppStorage("tapped") var tapped: Int = 0
     20     @AppStorage("dragged") var dragged: Int = 0
     21 
     22     @State private var offset = CGSize.zero
     23     @State private var didDrag = false
     24 
     25     private func hopAction(_ newValue: Int) {
     26         if tapped >= TAPPED && dragged < DRAGGED {
     27             withAnimation(Animation.easeOut(duration: DRAGDURATION).delay(DRAGDELAY)) {
     28                 offset.height = -50
     29             }
     30             withAnimation(Animation.easeOut(duration: DRAGSPEED).delay(DRAGDELAY + DRAGDURATION + DRAGSPEED)) {
     31                 offset.height = 10
     32             }
     33             withAnimation(Animation.easeOut(duration: DRAGSPEED/2).delay(DRAGDELAY + DRAGDURATION + 2.5*DRAGSPEED)) {
     34                 offset.height = 0
     35             }
     36         }
     37     }
     38 
     39     private func dragGesture() -> some Gesture {
     40         DragGesture(minimumDistance: 6)
     41             .onChanged { gesture in
     42                 var trans = gesture.translation
     43                 trans.width = .zero
     44                 if trans.height < -20 {
     45                     symLog.log(".onChanged: didDrag \(trans.height)")
     46                     withAnimation {
     47                         offset = .zero
     48                     }
     49                     didDrag = true
     50                     onDrag(stack.push())    // switch to camera
     51                     if tapped >= TAPPED {
     52                         dragged += 1
     53                     }
     54                 } else if trans.height < 0 {    // only drag up
     55                     symLog.log(".onChanged: \(trans.height)")
     56                     withAnimation {
     57                         offset = trans
     58                     }
     59                 }
     60             }
     61             .onEnded { gesture in
     62                 let trans = gesture.translation
     63                 if didDrag {
     64                     symLog.log(".onEnded: didDrag \(trans.height)")
     65                     didDrag = false
     66                 } else {
     67                     symLog.log(".onActionTab: \(trans.height)")
     68                     onTap()
     69                 }
     70                 withAnimation {
     71                     offset = .zero
     72                 }
     73             }
     74     }
     75 
     76     var body: some View {
     77         let vStack = VStack(spacing: 0) {
     78     // show floating actions
     79             let withText = if #available(iOS 26.0, *) { false } else { tapped < TAPPED }
     80             let width = withText ? 48 : 72.0
     81             let height = withText ? 36 : 57.6
     82             let image = tab.image
     83                 .resizable()
     84                 .scaledToFill()
     85                 .frame(width: width, height: height)
     86                 .clipped() // Crop the image to the frame size
     87 //                .opacity(1 - Double(abs(offset.height / 35)))
     88                 .gesture(dragGesture())
     89                 .onLongPressGesture(minimumDuration: 0.4) {
     90                     onDrag(stack.push())
     91                 }
     92                 .onTapGesture {
     93                     onTap()
     94                     tapped += 1
     95                 }
     96             if #available(iOS 26.0, *) {
     97                 image
     98             } else {
     99                 image
    100                     .padding(.bottom, 4)
    101                     .offset(offset)
    102             }
    103             if withText {
    104                 Text(tab.title)
    105                     .lineLimit(1)
    106                     .talerFont(.body)
    107             }
    108         }   .id(tab)
    109 //            .foregroundColor(isActive ? WalletColors().primaryAccent : .secondary)
    110             .padding(.vertical, 4)
    111             .accessibilityElement(children: .combine)
    112             .accessibility(label: Text(tab.title))
    113             .accessibility(addTraits: [.isButton])
    114             .accessibility(removeTraits: [.isImage])
    115             .onChange(of: controller.userAction) { newValue in
    116                 hopAction(newValue)         // make Action button jump to indicate it can be dragged
    117             }
    118 
    119         if #available(iOS 26.0, *) {
    120             vStack
    121                 .padding(.horizontal, 6)
    122                 .glassEffect(.clear.interactive())
    123                 .offset(offset)
    124         } else {
    125             vStack
    126                 .frame(maxWidth: .infinity)
    127         }
    128     }
    129 }
    130 // MARK: -
    131 //struct TabBarItem: View {
    132 //    let tab: TalerTab
    133 //
    134 //    var body: some View {
    135 //    }
    136 //}
    137 // MARK: -
    138 struct TabBarView: View {
    139     private let symLog = SymLogV(0)
    140     let stack: CallStack
    141     @Binding var selection: TalerTab
    142     @Binding var hidden: Int
    143     let onActionTab: () -> Void
    144     let onActionDrag: (CallStack) -> Void
    145 
    146     @Environment(\.keyboardShowing) var keyboardShowing
    147     @EnvironmentObject private var controller: Controller
    148 
    149     @AppStorage("minimalistic") var minimalistic: Bool = false
    150     @AppStorage("tapped") var tapped: Int = 0
    151 
    152     @Namespace private var namespace
    153 
    154     private func tabBarItem(for tab: TalerTab) -> some View {
    155         
    156         let isActive = selection == tab
    157         let vStack = VStack(spacing: 0) {
    158             let withText = tapped < TAPPED || !minimalistic
    159             let size = withText ? 24.0 : 36.0
    160             tab.image
    161                 .resizable()
    162                 .renderingMode(.template)
    163                 .tint(.black)
    164                 .aspectRatio(contentMode: .fit)
    165                 .frame(width: size, height: size)
    166 
    167             if withText {
    168                 if isActive {
    169                     Text(tab.title)
    170                         .bold()
    171                         .lineLimit(1)
    172                         .talerFont(.picker)
    173                 } else {
    174                     Text(tab.title)
    175                         .lineLimit(1)
    176                         .talerFont(.body)
    177                 }
    178             }
    179         }   .id(tab)
    180             .foregroundColor(isActive ? WalletColors().primaryAccent : .secondary)
    181             .padding(.vertical, 8)
    182             .accessibilityElement(children: .combine)
    183             .accessibility(label: Text(tab.title))
    184             .accessibility(addTraits: [.isButton])
    185             .accessibility(removeTraits: [.isImage])
    186         return vStack
    187             .frame(maxWidth: .infinity)
    188             .contentShape(Rectangle())
    189     }
    190 
    191     var body: some View {
    192         Group {
    193             if keyboardShowing || hidden > 0 {
    194                 EmptyView()
    195             } else {
    196                 let actionTab = ActionItem(stack: stack.push(),
    197                                              tab: TalerTab.actions,
    198                                            onTap: onActionTab,
    199                                           onDrag: onActionDrag)
    200                 let balanceTab = tabBarItem(for: TalerTab.balances)
    201                     .onTapGesture {
    202                         selection = .balances
    203                         controller.userAction += 1
    204                     }
    205                 let settingsTab = tabBarItem(for: TalerTab.settings)
    206                     .onTapGesture {
    207                         selection = .settings
    208                         controller.userAction += 1
    209                     }
    210                 HStack(alignment: .bottom) {
    211                     balanceTab
    212                     actionTab
    213                     settingsTab
    214                 }
    215                     .background(WalletColors().backgroundColor.ignoresSafeArea(edges: .bottom))
    216             }
    217         }
    218     }
    219 }
    220 // MARK: -
    221 //#Preview {
    222 //    TabBarView()
    223 //}