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