taler-ios

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

commit 0c31cb12e84eccb5334760a2f102ea9a0fff424c
parent 05428f8e53f24cac87b50f22a17d100f3ef9e242
Author: Marc Stibane <marc@taler.net>
Date:   Sun,  1 Mar 2026 21:56:04 +0100

QR scanner with X and torch

Diffstat:
MTalerWallet1/Views/Actions/ActionsSheet.swift | 24+++++++++++++++++-------
MTalerWallet1/Views/Main/MainView.swift | 4+++-
MTalerWallet1/Views/Sheets/QRSheet.swift | 44++++++++++++++++++++++++++++++++++++++++----
3 files changed, 60 insertions(+), 12 deletions(-)

diff --git a/TalerWallet1/Views/Actions/ActionsSheet.swift b/TalerWallet1/Views/Actions/ActionsSheet.swift @@ -141,16 +141,20 @@ struct ActionsSheet: View { let depoDisabled = !mayDeposit /// [􁉇 Send ] [􁉅 Request] SendRequestV(stack: stack.push(), sendDisabled: sendDisabled, recvDisabled: recvDisabled) + .accessibility(sortPriority: 1) // read this after maps /// [􁾩Deposit] [􁾭 Withdraw] DepositWithdrawV(stack: stack.push(), sendDisabled: depoDisabled, recvDisabled: noBalances) + .accessibility(sortPriority: 0) // read this last /// [􀎻 Scan QR ] QRButton(hideTitle: false) { qrButtonTapped = true } .lineLimit(5) .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .center)) + .accessibility(sortPriority: 3) // read this first } .padding() + .accessibilityElement(children: .contain) } } // MARK: - @@ -164,6 +168,8 @@ struct DualHeightSheet: View { let logger = Logger(subsystem: "net.taler.gnu", category: "DualSheet") @Environment(\.colorScheme) private var colorScheme @Environment(\.colorSchemeContrast) private var colorSchemeContrast + + @AppStorage("minimalistic") var minimalistic: Bool = false // @State private var selectedDetent: PresentationDetent = scanDetent // Cannot use instance member 'scanDetent' within property initializer @State private var selectedDetent: PresentationDetent = .fraction(0.1) // workaround - update in .task @State private var detents: Set<PresentationDetent> = [.fraction(0.1)] // " @@ -173,13 +179,15 @@ struct DualHeightSheet: View { @State private var sheetHeight: CGFloat = .zero let scanDetent: PresentationDetent = .fraction(SCANDETENT) + let actionDetent: PresentationDetent = .fraction(ACTIONDETENT) func updateDetentsWithDelay() { Task { //(1 second = 1_000_000_000 nanoseconds) try? await Task.sleep(nanoseconds: 80_000_000) - guard selectedDetent == scanDetent else { return } - detents = [scanDetent] + let detent = minimalistic ? scanDetent : actionDetent + guard selectedDetent == detent else { return } + detents = [detent] logger.trace("❗️detents = [scanDetent]") // 0.999 % } } @@ -204,7 +212,8 @@ struct DualHeightSheet: View { qrButtonTapped = true // tell our caller withAnimation(Animation.easeIn(duration: 0.6)) { // animate this sheet to full height - selectedDetent = scanDetent + let detent = minimalistic ? scanDetent : actionDetent + selectedDetent = detent } } } @@ -224,13 +233,14 @@ struct DualHeightSheet: View { } .presentationDetents(detents, selection: $selectedDetent) .onChange(of: selectedDetent) { newValue in - if newValue == scanDetent { // user swiped the sheet up to activate QR scanner + let detent = minimalistic ? scanDetent : actionDetent + if newValue == detent { // user swiped the sheet up to activate QR scanner logger.trace("onChange❗️selectedDetent = \(SCANDETENT)") updateDetentsWithDelay() qrButtonTapped = true // tell our caller } else { // SwiftUI "innerHeight" determined how big the half sheet should be logger.trace("onChange❗️selectedDetent = .height(\(sheetHeight))") - detents = [scanDetent, .height(sheetHeight)] + detents = [detent, .height(sheetHeight)] } } .task { @@ -250,11 +260,11 @@ struct DualHeightSheet: View { let qrSheet = AnyView(QRSheet(stack: stack.push(".sheet"), selectedBalance: selectedBalance, scannedSomething: $scannedCode)) - let detent: PresentationDetent = .fraction(scannedCode ? ACTIONDETENT : SCANDETENT) + let detent: PresentationDetent = .fraction(scannedCode ? ACTIONDETENT + : minimalistic ? SCANDETENT : ACTIONDETENT) Sheet(stack: stack.push(), sheetView: qrSheet) .presentationDetents([detent]) .transition(.opacity) - } } else { scrollView diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -27,6 +27,7 @@ struct MainView: View { @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel @EnvironmentObject private var biometricService: BiometricService + @AppStorage("minimalistic") var minimalistic: Bool = false @AppStorage("talerFontIndex") var talerFontIndex: Int = 0 // extension mustn't define this, so it must be here @AppStorage("playSoundsI") var playSoundsI: Int = 1 // extension mustn't define this, so it must be here @AppStorage("playSoundsB") var playSoundsB: Bool = false @@ -194,7 +195,8 @@ struct MainView: View { scannedSomething: $scannedCode)) // let _ = logger.trace("❗️showScanner: \(SCANDETENT)❗️") if #available(iOS 16.4, *) { - let detent: PresentationDetent = .fraction(scannedCode ? ACTIONDETENT : SCANDETENT) + let detent: PresentationDetent = .fraction(scannedCode ? ACTIONDETENT + : minimalistic ? SCANDETENT : ACTIONDETENT) Sheet(stack: stack.push(), sheetView: qrSheet) .presentationDetents([detent]) .transition(.opacity) diff --git a/TalerWallet1/Views/Sheets/QRSheet.swift b/TalerWallet1/Views/Sheets/QRSheet.swift @@ -17,6 +17,8 @@ struct QRSheet: View { @Binding var scannedSomething: Bool @EnvironmentObject private var model: WalletModel + @AppStorage("minimalistic") var minimalistic: Bool = false + @State private var scannedCode: String? @State private var urlToOpen: URL? = nil @State private var isTorchOn: Bool = false @@ -69,9 +71,10 @@ struct QRSheet: View { } } } else { - CodeScannerView(codeTypes: [AVMetadataObject.ObjectType.qr], - showViewfinder: true, - isTorchOn: isTorchOn) { response in + let codeScannerView = CodeScannerView(codeTypes: [AVMetadataObject.ObjectType.qr], + showViewfinder: true, + isTorchOn: isTorchOn + ) { response in let closingAnnouncement: String switch response { case .success(let result): @@ -88,7 +91,40 @@ struct QRSheet: View { } announce(closingAnnouncement) } - .onTapGesture { isTorchOn.toggle() } + + if minimalistic { + codeScannerView + .onTapGesture { isTorchOn.toggle() } + } else if #available(iOS 16.4, *) { + codeScannerView + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + dismissTop(stack.push()) + } label: { + Image(systemName: "xmark") + } + .accessibilityHidden(true) // VoiceOver has its own "Dismiss Popup" button + } + ToolbarItem(placement: .topBarTrailing) { + let a11yValue = isTorchOn ? String(localized: "on", comment: "a11y") + : String(localized: "off", comment: "a11y") + Button { + isTorchOn.toggle() + } label: { + Image(systemName: isTorchOn ? "lightbulb.fill" : "lightbulb") + } + .accessibilityLabel(Text("Torch for QR code scanning", comment: "a11y")) + .accessibilityValue(Text(a11yValue)) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.gray) + .toolbarBackground(.visible) + } else { + codeScannerView + .onTapGesture { isTorchOn.toggle() } + } } } }