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 //}