Buttons.swift (16269B)
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 Foundation 10 import AVFoundation 11 12 extension ShapeStyle where Self == Color { 13 static var random: Color { 14 Color( 15 red: .random(in: 0...1), 16 green: .random(in: 0...1), 17 blue: .random(in: 0...1) 18 ) 19 } 20 } 21 22 struct HamburgerButton : View { 23 let action: () -> Void 24 25 var body: some View { 26 Button(action: action) { 27 Image(systemName: HAMBURGER) 28 // Image(systemName: "sidebar.squares.leading") // 29 } 30 .talerFont(.title) 31 .accessibilityLabel(Text("Main Menu", comment: "a11y")) 32 .accessibilityIdentifier(A11yID.hamburgerButton) 33 } 34 } 35 36 struct LinkButton: View { 37 let destination: URL 38 let hintTitle: String 39 let buttonTitle: String 40 let a11yHint: String 41 let badge: String 42 43 @AppStorage("minimalistic") var minimalistic: Bool = false 44 45 var body: some View { 46 VStack(alignment: .leading) { 47 if !minimalistic { // show hint that the user should authorize on bank website 48 Text(hintTitle) 49 .fixedSize(horizontal: false, vertical: true) // wrap in scrollview 50 .multilineTextAlignment(.leading) // otherwise 51 .listRowSeparator(.hidden) 52 } 53 Link(destination: destination) { 54 HStack(spacing: 8.0) { 55 Image(systemName: LINK) 56 Text(buttonTitle) 57 } 58 } 59 .buttonStyle(TalerButtonStyle(type: .prominent, badge: badge)) 60 .accessibilityHint(a11yHint) 61 } 62 } 63 } 64 65 struct QRButton : View { 66 let hideTitle: Bool 67 let action: () -> Void 68 69 @AppStorage("minimalistic") var minimalistic: Bool = false 70 @State private var showCameraAlert: Bool = false 71 72 private var openSettingsButton: some View { 73 Button("Open Settings") { 74 showCameraAlert = false 75 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 76 } 77 } 78 let closingAnnouncement = String(localized: "Closing Camera", comment: "a11y") 79 80 var defaultPriorityAnnouncement = String(localized: "Opening Camera", comment: "a11y") 81 82 var highPriorityAnnouncement: AttributedString { 83 var highPriorityString = AttributedString(localized: "Camera Active", comment: "a11y") 84 if #available(iOS 17.0, *) { 85 highPriorityString.accessibilitySpeechAnnouncementPriority = .high 86 } 87 return highPriorityString 88 } 89 @MainActor 90 private func checkCameraAvailable() -> Void { 91 // Open Camera when QR-Button was tapped 92 announce(defaultPriorityAnnouncement) 93 94 AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in 95 if granted { 96 action() 97 if #available(iOS 17.0, *) { 98 AccessibilityNotification.Announcement(highPriorityAnnouncement).post() 99 } else { 100 let cameraActive = String(localized: "Camera Active", comment: "a11y") 101 announce(cameraActive) 102 } 103 } else { 104 showCameraAlert = true 105 } 106 }) 107 } 108 109 var body: some View { 110 let dismissAlertButton = Button("Cancel", role: .cancel) { 111 announce(closingAnnouncement) 112 showCameraAlert = false 113 } 114 let scanText = String(localized: "Scan QR code", comment: "Button title, a11y") 115 let qrImage = Image(systemName: QRBUTTON) 116 let qrText = Text(qrImage) 117 Button(action: checkCameraAvailable) { 118 if hideTitle { 119 qrImage 120 .resizable() 121 .scaledToFit() 122 .foregroundStyle(WalletColors().primaryAccent) 123 } else if minimalistic { 124 let width = UIScreen.screenWidth / 7 125 qrText.talerFont(.largeTitle) 126 .padding(.horizontal, width) 127 } else { 128 HStack(spacing: 16) { 129 qrText.talerFont(.title) 130 Text(scanText) 131 }.padding(.horizontal) 132 } 133 } 134 .accessibilityLabel(scanText) 135 .accessibilityIdentifier(A11yID.qrButton) 136 .alert("Scanning QR-codes requires access to the camera", 137 isPresented: $showCameraAlert, 138 actions: { openSettingsButton 139 dismissAlertButton }, 140 message: { Text("Please allow camera access in Settings.") }) // Scanning QR-codes 141 } 142 } 143 144 struct DoneButton : View { 145 let titleStr: String? 146 let accessibilityLabelStr: String 147 let action: () -> Void 148 149 var body: some View { 150 Button(action: action) { 151 if let titleStr { 152 Text(titleStr) 153 } else { 154 Image(systemName: CHECKMARK) // 155 } 156 } 157 .tint(WalletColors().primaryAccent) 158 .talerFont(.title) 159 .accessibilityLabel(accessibilityLabelStr) 160 .accessibilityIdentifier(A11yID.doneButton) 161 } 162 } 163 164 struct PlusButton : View { 165 let accessibilityLabelStr: String 166 let action: () -> Void 167 168 var body: some View { 169 Button(action: action) { 170 Image(systemName: PLUS) 171 } 172 .tint(WalletColors().primaryAccent) 173 .talerFont(.title) 174 .accessibilityLabel(accessibilityLabelStr) 175 .accessibilityIdentifier(A11yID.plusButton) 176 } 177 } 178 179 struct MoreButton : View { 180 let accessibilityLabelStr: String 181 let action: () -> Void 182 183 var body: some View { 184 Button(action: action) { 185 Image(systemName: MORE) 186 } 187 .tint(WalletColors().primaryAccent) 188 .talerFont(.title) 189 .accessibilityLabel(accessibilityLabelStr) 190 .accessibilityIdentifier(A11yID.moreButton) 191 } 192 } 193 194 @available(iOS 26.0, *) 195 struct SettingsButton26 : View { 196 let sortPriority: Double 197 198 var body: some View { 199 Button { 200 // will trigger NavigationLink 201 NotificationCenter.default.post(name: .SettingsAction, object: nil) 202 } label: { 203 Label(TalerTab.settings.title, systemImage: TalerTab.settings.sysImg) 204 .labelStyle(.iconOnly) 205 } 206 // .padding() 207 .buttonStyle(.glass) 208 .accessibilitySortPriority(sortPriority) 209 } 210 } 211 212 struct SettingsButton : View { 213 let accessibilityLabelStr: String 214 let action: () -> Void 215 216 var body: some View { 217 let button = Button(action: action) { 218 Image(systemName: SETTINGS) 219 } 220 .tint(WalletColors().primaryAccent) 221 .talerFont(.title) 222 .accessibilityLabel(accessibilityLabelStr) 223 .accessibilityIdentifier(A11yID.settingsButton) 224 // if #available(iOS 26.0, *) { 225 // return button.buttonStyle(.glass) 226 // } 227 return button 228 } 229 } 230 struct TalerPasteButton : View { 231 let accessibilityLabelStr: String 232 let action: () -> Void 233 234 var body: some View { 235 let button = Button(action: action) { 236 Image(systemName: PASTE) 237 } 238 .tint(WalletColors().primaryAccent) 239 .talerFont(.title) 240 .accessibilityLabel(accessibilityLabelStr) 241 .accessibilityIdentifier(A11yID.pasteButton) 242 // if #available(iOS 26.0, *) { 243 // return button.buttonStyle(.glass) 244 // } 245 return button 246 } 247 } 248 249 struct ZoomInButton : View { 250 let accessibilityLabelStr: String 251 let action: () -> Void 252 253 var body: some View { 254 Button(action: action) { 255 Image(ICONNAME_ZOOM_IN, SYSTEM_ZOOM_IN) 256 } 257 .tint(WalletColors().primaryAccent) 258 .talerFont(.title) 259 .accessibilityLabel(accessibilityLabelStr) 260 .accessibilityIdentifier(A11yID.zoomInButton) 261 } 262 } 263 264 struct ZoomOutButton : View { 265 let accessibilityLabelStr: String 266 let action: () -> Void 267 268 var body: some View { 269 Button(action: action) { 270 Image(ICONNAME_ZOOM_OUT, SYSTEM_ZOOM_OUT) 271 } 272 .tint(WalletColors().primaryAccent) 273 .talerFont(.title) 274 .accessibilityLabel(accessibilityLabelStr) 275 .accessibilityIdentifier(A11yID.zoomOutButton) 276 } 277 } 278 279 struct BackButton : View { 280 let action: () -> Void 281 282 var body: some View { 283 Button(action: action) { 284 let name = ICONNAME_INCOMING + ICONNAME_FILL 285 let sysName = SYSTEM_INCOMING4 + ICONNAME_FILL 286 Image(name, sysName, fallback: FALLBACK_INCOMING) 287 } 288 .tint(WalletColors().primaryAccent) 289 .talerFont(.largeTitle) 290 .accessibilityLabel(Text("Back", comment: "a11y")) 291 .accessibilityIdentifier(A11yID.backButton) 292 } 293 } 294 295 struct ForwardButton : View { 296 let enabled: Bool 297 let action: () -> Void 298 299 var body: some View { 300 let myAction = { 301 if enabled { 302 action() 303 } 304 } 305 Button(action: myAction) { 306 let imageName = enabled ? ICONNAME_OUTGOING + ICONNAME_FILL 307 : ICONNAME_OUTGOING 308 let sysName = enabled ? SYSTEM_OUTGOING4 + ICONNAME_FILL 309 : SYSTEM_OUTGOING4 310 Image(imageName, sysName, fallback: FALLBACK_OUTGOING) 311 } 312 .tint(WalletColors().primaryAccent) 313 .talerFont(.largeTitle) 314 .accessibilityLabel(Text("Continue", comment: "a11y")) 315 .accessibilityIdentifier(A11yID.forwardButton) 316 } 317 } 318 319 struct ArrowUpButton : View { 320 let action: () -> Void 321 322 var body: some View { 323 Button(action: action) { 324 Image(systemName: ARROW_TOP) // 325 } 326 .tint(WalletColors().primaryAccent) 327 .talerFont(.title2) 328 .accessibilityLabel(Text("Scroll up", comment: "a11y")) 329 .accessibilityIdentifier(A11yID.arrowUpButton) 330 } 331 } 332 333 struct ArrowDownButton : View { 334 let action: () -> Void 335 336 var body: some View { 337 Button(action: action) { 338 Image(systemName: ARROW_BOT) // 339 } 340 .tint(WalletColors().primaryAccent) 341 .talerFont(.title2) 342 .accessibilityLabel(Text("Scroll down", comment: "a11y")) 343 .accessibilityIdentifier(A11yID.arrowDownButton) 344 } 345 } 346 347 struct ReloadButton : View { 348 let disabled: Bool 349 let action: () -> Void 350 351 var body: some View { 352 Button(action: action) { 353 Image(systemName: RELOAD) // 354 } 355 .tint(WalletColors().primaryAccent) 356 .talerFont(.title) 357 .accessibilityLabel(Text("Reload", comment: "a11y")) 358 .accessibilityIdentifier(A11yID.reloadButton) 359 .disabled(disabled) 360 } 361 } 362 363 enum TalerButtonStyleType { 364 case plain 365 case bordered 366 case prominent 367 } 368 struct TalerButtonStyle: ButtonStyle { 369 var type: TalerButtonStyleType = .plain 370 var dimmed: Bool = false 371 var narrow: Bool = false 372 var one: Bool = false 373 var disabled: Bool = false 374 var aligned: TextAlignment = .center 375 var badge: String = EMPTYSTRING 376 377 @Environment(\.colorScheme) private var colorScheme 378 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 379 380 public func makeBody(configuration: ButtonStyleConfiguration) -> some View { 381 // configuration.role = type == .prominent ? .primary : .normal Only on macOS 382 MyBigButton(foreColor: foreColor(type: type, pressed: configuration.isPressed, 383 scheme: colorScheme, contrast: colorSchemeContrast, disabled: disabled), 384 backColor: backColor(type: type, pressed: configuration.isPressed, disabled: disabled), 385 dimmed: dimmed, 386 configuration: configuration, 387 disabled: disabled, 388 narrow: narrow, 389 one: one, 390 aligned: aligned, 391 badge: badge, 392 scheme: colorScheme, 393 contrast: colorSchemeContrast) 394 } 395 396 func foreColor(type: TalerButtonStyleType, 397 pressed: Bool, 398 scheme: ColorScheme, 399 contrast: ColorSchemeContrast, 400 disabled: Bool) -> Color { 401 if type == .plain { 402 return WalletColors().fieldForeground // primary text color 403 } 404 return WalletColors().buttonForeColor(pressed: pressed, 405 disabled: disabled, 406 scheme: scheme, 407 contrast: contrast, 408 prominent: type == .prominent) 409 } 410 func backColor(type: TalerButtonStyleType, pressed: Bool, disabled: Bool) -> Color { 411 if type == .plain && !pressed { 412 return Color.clear 413 } 414 return WalletColors().buttonBackColor(pressed: pressed, 415 disabled: disabled, 416 prominent: type == .prominent) 417 } 418 419 struct BackgroundView: View { 420 let color: Color 421 let dimmed: Bool 422 var body: some View { 423 RoundedRectangle( 424 cornerRadius: 15, 425 style: .continuous 426 ) 427 .fill(color) 428 .opacity(dimmed ? 0.6 : 1.0) 429 } 430 } 431 432 struct MyBigButton: View { 433 // var type: TalerButtonStyleType 434 let foreColor: Color 435 let backColor: Color 436 let dimmed: Bool 437 let configuration: ButtonStyle.Configuration 438 let disabled: Bool 439 let narrow: Bool 440 let one: Bool 441 let aligned: TextAlignment 442 var badge: String 443 var scheme: ColorScheme 444 var contrast: ColorSchemeContrast 445 446 var body: some View { 447 let aligned2: Alignment = (aligned == .center) ? Alignment.center 448 : (aligned == .leading) ? Alignment.leading 449 : Alignment.trailing 450 let hasBadge = !badge.isEmpty 451 let buttonLabel = configuration.label 452 .multilineTextAlignment(aligned) 453 .lineLimit(one ? 1 : 3) 454 .talerFont(.title3) // narrow ? .title3 : .title2 455 .frame(maxWidth: narrow ? nil : .infinity, alignment: aligned2) 456 .padding(.vertical, 10) 457 .padding(.horizontal, hasBadge ? 0 : 6) 458 .foregroundColor(foreColor) 459 .background(BackgroundView(color: backColor, dimmed: dimmed)) 460 .contentShape(Rectangle()) // make sure the button can be pressed even if backgroundColor == clear 461 .scaleEffect(configuration.isPressed ? 0.95 : 1) 462 .animation(.spring(response: 0.1), value: configuration.isPressed) 463 .disabled(disabled) 464 if hasBadge { 465 let badgeColor: Color = (badge == CONFIRM_BANK) ? WalletColors().confirm 466 : WalletColors().attention 467 let badgeV = Image(systemName: badge) 468 .talerFont(.caption) 469 HStack(alignment: .top, spacing: 0) { 470 badgeV.foregroundColor(.clear) 471 buttonLabel 472 badgeV.foregroundColor(badgeColor) 473 } 474 } else { 475 buttonLabel 476 } 477 } 478 } 479 } 480 // MARK: - 481 #if DEBUG 482 fileprivate struct ContentView_Previews: PreviewProvider { 483 static var previews: some View { 484 let testButtonTitle = String("Placeholder") 485 Button(testButtonTitle) {} 486 .buttonStyle(TalerButtonStyle(type: .bordered, aligned: .trailing)) 487 } 488 } 489 #endif