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/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 6ebe4a4..3e84539 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. When `nil` or empty no label is set (default behavior). + 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) ) + .accessibilityLabelIfPresent(accessibilityText) } } diff --git a/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift b/Sources/OSBarcodeLib/Scanner/Interface Elements/OSBARCTorchButton.swift index d82fce1..d74b5e0 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. 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" /// 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 { ) } } + .accessibilityLabelIfPresent(accessibilityText) } } diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift index 8749efa..f82364e 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerBehaviour.swift @@ -6,7 +6,7 @@ import SwiftUI final class OSBARCScannerBehaviour: OSBARCCoordinatable, OSBARCScannerProtocol { /// 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 +31,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 - + + // 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, shouldShowButton, @@ -49,7 +54,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..e774f11 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. `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 @@ -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? {