From d5d16577c559107ada19530f310005b380b2713a Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Thu, 11 Jun 2026 17:08:56 -0400 Subject: [PATCH 1/2] feat: add accessibility labels to torch and cancel buttons The torch and cancel buttons were icon-only with no alternative text, so screen readers could not announce them. Add default English accessibility labels and expose optional cancelButtonAccessibilityLabel, torchButtonOnAccessibilityLabel and torchButtonOffAccessibilityLabel scan parameters so consumers can customize/localize them. Closes #44 --- .../Models/OSBARCScanParameters.swift | 27 ++++++++++++++----- .../OSBARCCancelButton.swift | 5 +++- .../OSBARCTorchButton.swift | 11 ++++++-- .../Scanner/OSBARCScannerBehaviour.swift | 21 ++++++++++++--- .../Scanner/OSBARCScannerView.swift | 17 +++++++++--- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/Sources/OSBarcodeLib/Models/OSBARCScanParameters.swift b/Sources/OSBarcodeLib/Models/OSBARCScanParameters.swift index 80ae389..9473a33 100644 --- a/Sources/OSBarcodeLib/Models/OSBARCScanParameters.swift +++ b/Sources/OSBarcodeLib/Models/OSBARCScanParameters.swift @@ -1,28 +1,43 @@ public struct OSBARCScanParameters { /// Text to be displayed on the scanner view. public let scanInstructions: String - + /// Text to be displayed for the scan button, if this is configured. `Nil` value means that the button will not be shown. public let scanButtonText: String? - + // Camera to use for input gathering. public let cameraDirection: OSBARCCameraModel - + // Scanner view's orientation. public let scanOrientation: OSBARCOrientationModel - + // The optional hint, to scan a specific format (e.g. only qr code). `Nil` or `unknown` value means it can scan all. public let hint: OSBARCScannerHint? - + + /// Optional accessibility label for the cancel button. `Nil` or empty value uses the library's default. + public let cancelButtonAccessibilityLabel: String? + + /// Optional accessibility label for the torch button when the torch is on. `Nil` or empty value uses the library's default. + public let torchButtonOnAccessibilityLabel: String? + + /// Optional accessibility label for the torch button when the torch is off. `Nil` or empty value uses the library's default. + public let torchButtonOffAccessibilityLabel: String? + public init(scanInstructions: String, scanButtonText: String?, cameraDirection: OSBARCCameraModel, scanOrientation: OSBARCOrientationModel, - hint: OSBARCScannerHint?) { + hint: OSBARCScannerHint?, + cancelButtonAccessibilityLabel: String? = nil, + torchButtonOnAccessibilityLabel: String? = nil, + torchButtonOffAccessibilityLabel: String? = nil) { self.scanInstructions = scanInstructions self.scanButtonText = scanButtonText self.cameraDirection = cameraDirection self.scanOrientation = scanOrientation self.hint = hint + self.cancelButtonAccessibilityLabel = cancelButtonAccessibilityLabel + self.torchButtonOnAccessibilityLabel = torchButtonOnAccessibilityLabel + self.torchButtonOffAccessibilityLabel = torchButtonOffAccessibilityLabel } } diff --git a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift index 6ebe4a4..39034e3 100644 --- a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift +++ b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift @@ -5,7 +5,9 @@ import SwiftUI struct OSBARCCancelButton: View { /// The action performed when the button is clicked. let action: () -> Void - + /// The accessibility label read by screen readers. + let accessibilityText: String + /// The icon to display.. private let cancelIcon: String = "xmark" /// The scale to apply to the icon to display. @@ -31,5 +33,6 @@ struct OSBARCCancelButton: View { Circle() .foregroundStyle(forColour: backgroundColour) ) + .accessibility(label: Text(accessibilityText)) } } diff --git a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift index d82fce1..0504902 100644 --- a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift +++ b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift @@ -6,7 +6,11 @@ struct OSBARCTorchButton: View { let action: () -> Void /// Indicates if the feature is enabled or not. let isOn: Bool - + /// The accessibility label read by screen readers when the torch is on. + let onAccessibilityText: String + /// The accessibility label read by screen readers when the torch is off. + let offAccessibilityText: String + /// Image name to be shown on the button. private let imageName: String = "flash" /// Height and width to use (the button is squared). @@ -24,7 +28,9 @@ struct OSBARCTorchButton: View { private var iconName: String { "\(imageName)\(isOn ? "-selected" : "")" } /// Calculates the background colour to be used based on the toggle value. private var backgroundColour: Color { isOn ? selectedBackgroundColour : notSelectedBackgroundColour } - + /// Calculates the accessibility label to be used based on the toggle value. + private var accessibilityText: String { isOn ? onAccessibilityText : offAccessibilityText } + var body: some View { Button(action: action) { Image(iconName, bundle: Bundle.imageBundle) @@ -40,6 +46,7 @@ struct OSBARCTorchButton: View { ) } } + .accessibility(label: Text(accessibilityText)) } } diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift index 8749efa..5bc5db5 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift @@ -4,9 +4,16 @@ import SwiftUI /// Class responsible for the barcode scanner view flow. final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { + /// Default English accessibility labels, used when the consumer does not provide custom ones. + private enum AccessibilityDefaults { + static let cancelLabel = "Cancel scanning" + static let torchOnLabel = "Turn off flashlight" + static let torchOffLabel = "Turn on flashlight" + } + /// A publisher value responsible for the resulting scanned value. @Published private var scanResult: OSBARCScanResult = OSBARCScanResult.empty() - + /// The publisher's cancellable instance collector. private var cancellables: Set = [] @@ -31,7 +38,12 @@ final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { let buttonText = parameters.scanButtonText ?? "" // not having the button enabled is translated into having an empty text. let shouldShowButton = !buttonText.isEmpty // if empty text is passed, the button is not enabled on the scanner view - + + // resolve the accessibility labels, falling back to the library's defaults when not provided. + let cancelAccessibilityLabel = parameters.cancelButtonAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.cancelLabel + let torchOnAccessibilityLabel = parameters.torchButtonOnAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.torchOnLabel + let torchOffAccessibilityLabel = parameters.torchButtonOffAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.torchOffLabel + let barcodeDecoder = OSBARCCaptureOutputDecoder( scanResultBinding, shouldShowButton, @@ -49,7 +61,10 @@ final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { instructionsText: parameters.scanInstructions, buttonText: buttonText, shouldShowButton: shouldShowButton, - deviceType: UIDevice.current.userInterfaceIdiom.deviceTypeModel + deviceType: UIDevice.current.userInterfaceIdiom.deviceTypeModel, + cancelAccessibilityLabel: cancelAccessibilityLabel, + torchOnAccessibilityLabel: torchOnAccessibilityLabel, + torchOffAccessibilityLabel: torchOffAccessibilityLabel ) let hostingController = OSBARCScannerViewHostingController(rootView: scannerView, parameters.scanOrientation) hostingController.modalPresentationStyle = .fullScreen diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift index 1d9f7eb..c59898c 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift @@ -21,7 +21,14 @@ struct OSBARCScannerView: View { /// The type of device being used. let deviceType: OSBARCDeviceTypeModel - + + /// Accessibility label for the cancel button. + let cancelAccessibilityLabel: String + /// Accessibility label for the torch button when the torch is on. + let torchOnAccessibilityLabel: String + /// Accessibility label for the torch button when the torch is off. + let torchOffAccessibilityLabel: String + /// Frame of portion of the screen used for scanning. @State private var scanFrame: CGRect = .zero @@ -48,9 +55,9 @@ struct OSBARCScannerView: View { /// Cancel button. private var cancelButton: OSBARCCancelButton { - .init { + .init(action: { scanResult = OSBARCScanResult.empty() // cancelling translates in scanResult being empty. - } + }, accessibilityText: cancelAccessibilityLabel) } /// Scanning Instructions Text Field. @@ -114,7 +121,9 @@ struct OSBARCScannerView: View { private var torchButton: OSBARCTorchButton { .init(action: { viewModel.isTorchButtonOn.toggle() - }, isOn: viewModel.isTorchButtonOn) + }, isOn: viewModel.isTorchButtonOn, + onAccessibilityText: torchOnAccessibilityLabel, + offAccessibilityText: torchOffAccessibilityLabel) } private var zoomSelectorView: OSBARCZoomSelectorView? { From 288a739f2b1b7f2bf493915a09017cddb0eac688 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Sun, 14 Jun 2026 15:21:24 -0400 Subject: [PATCH 2/2] refactor: make button accessibility labels opt-in with no default When a label is not provided (nil/empty), no accessibility label is applied, keeping the previous behavior unchanged. The English default labels are removed; consumers supply the (localized) labels via the scan parameters. --- .../Scanner/Extensions/View+CustomModifiers.swift | 13 +++++++++++++ .../Interface Elements/OSBARCCancelButton.swift | 6 +++--- .../Interface Elements/OSBARCTorchButton.swift | 12 ++++++------ .../Scanner/OSBARCScannerBehaviour.swift | 15 ++++----------- .../OSBarcodeLib/Scanner/OSBARCScannerView.swift | 12 ++++++------ 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Sources/OSBarcodeLib/Scanner/Extensions/View+CustomModifiers.swift b/Sources/OSBarcodeLib/Scanner/Extensions/View+CustomModifiers.swift index 3c7e44e..114ce2b 100644 --- a/Sources/OSBarcodeLib/Scanner/Extensions/View+CustomModifiers.swift +++ b/Sources/OSBarcodeLib/Scanner/Extensions/View+CustomModifiers.swift @@ -28,6 +28,19 @@ extension View { } } + /// Applies an accessibility label only when a non-empty value is provided. + /// When `label` is `nil` or empty the view is left untouched, preserving the default behavior (no label). + /// - Parameter label: The accessibility label to apply, if any. + /// - Returns: Either the original `View` or the `View` with the accessibility label applied. + @ViewBuilder + func accessibilityLabelIfPresent(_ label: String?) -> some View { + if let label = label, !label.isEmpty { + self.accessibility(label: Text(label)) + } else { + self + } + } + /// Ignores safe area for different versions of iOS. /// - Returns: the View ignoring all safe areas. @ViewBuilder diff --git a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift index 39034e3..3e84539 100644 --- a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift +++ b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCCancelButton.swift @@ -5,8 +5,8 @@ import SwiftUI struct OSBARCCancelButton: View { /// The action performed when the button is clicked. let action: () -> Void - /// The accessibility label read by screen readers. - let accessibilityText: String + /// The accessibility label read by screen readers. When `nil` or empty no label is set (default behavior). + let accessibilityText: String? /// The icon to display.. private let cancelIcon: String = "xmark" @@ -33,6 +33,6 @@ struct OSBARCCancelButton: View { Circle() .foregroundStyle(forColour: backgroundColour) ) - .accessibility(label: Text(accessibilityText)) + .accessibilityLabelIfPresent(accessibilityText) } } diff --git a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift index 0504902..d74b5e0 100644 --- a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift +++ b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift @@ -6,10 +6,10 @@ struct OSBARCTorchButton: View { let action: () -> Void /// Indicates if the feature is enabled or not. let isOn: Bool - /// The accessibility label read by screen readers when the torch is on. - let onAccessibilityText: String - /// The accessibility label read by screen readers when the torch is off. - let offAccessibilityText: String + /// The accessibility label read by screen readers when the torch is on. When `nil` or empty no label is set (default behavior). + let onAccessibilityText: String? + /// The accessibility label read by screen readers when the torch is off. When `nil` or empty no label is set (default behavior). + let offAccessibilityText: String? /// Image name to be shown on the button. private let imageName: String = "flash" @@ -29,7 +29,7 @@ struct OSBARCTorchButton: View { /// Calculates the background colour to be used based on the toggle value. private var backgroundColour: Color { isOn ? selectedBackgroundColour : notSelectedBackgroundColour } /// Calculates the accessibility label to be used based on the toggle value. - private var accessibilityText: String { isOn ? onAccessibilityText : offAccessibilityText } + private var accessibilityText: String? { isOn ? onAccessibilityText : offAccessibilityText } var body: some View { Button(action: action) { @@ -46,7 +46,7 @@ struct OSBARCTorchButton: View { ) } } - .accessibility(label: Text(accessibilityText)) + .accessibilityLabelIfPresent(accessibilityText) } } diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift index 5bc5db5..f82364e 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift @@ -4,13 +4,6 @@ import SwiftUI /// Class responsible for the barcode scanner view flow. final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { - /// Default English accessibility labels, used when the consumer does not provide custom ones. - private enum AccessibilityDefaults { - static let cancelLabel = "Cancel scanning" - static let torchOnLabel = "Turn off flashlight" - static let torchOffLabel = "Turn on flashlight" - } - /// A publisher value responsible for the resulting scanned value. @Published private var scanResult: OSBARCScanResult = OSBARCScanResult.empty() @@ -39,10 +32,10 @@ final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { let buttonText = parameters.scanButtonText ?? "" // not having the button enabled is translated into having an empty text. let shouldShowButton = !buttonText.isEmpty // if empty text is passed, the button is not enabled on the scanner view - // resolve the accessibility labels, falling back to the library's defaults when not provided. - let cancelAccessibilityLabel = parameters.cancelButtonAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.cancelLabel - let torchOnAccessibilityLabel = parameters.torchButtonOnAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.torchOnLabel - let torchOffAccessibilityLabel = parameters.torchButtonOffAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } ?? AccessibilityDefaults.torchOffLabel + // accessibility labels are optional; when not provided (nil/empty) no label is set, preserving the default behavior. + let cancelAccessibilityLabel = parameters.cancelButtonAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } + let torchOnAccessibilityLabel = parameters.torchButtonOnAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } + let torchOffAccessibilityLabel = parameters.torchButtonOffAccessibilityLabel.flatMap { $0.isEmpty ? nil : $0 } let barcodeDecoder = OSBARCCaptureOutputDecoder( scanResultBinding, diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift index c59898c..e774f11 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerView.swift @@ -22,12 +22,12 @@ struct OSBARCScannerView: View { /// The type of device being used. let deviceType: OSBARCDeviceTypeModel - /// Accessibility label for the cancel button. - let cancelAccessibilityLabel: String - /// Accessibility label for the torch button when the torch is on. - let torchOnAccessibilityLabel: String - /// Accessibility label for the torch button when the torch is off. - let torchOffAccessibilityLabel: String + /// Accessibility label for the cancel button. `Nil`/empty means no label is set (default behavior). + let cancelAccessibilityLabel: String? + /// Accessibility label for the torch button when the torch is on. `Nil`/empty means no label is set (default behavior). + let torchOnAccessibilityLabel: String? + /// Accessibility label for the torch button when the torch is off. `Nil`/empty means no label is set (default behavior). + let torchOffAccessibilityLabel: String? /// Frame of portion of the screen used for scanning. @State private var scanFrame: CGRect = .zero