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:
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() }
+ }
}
}
}