Search code examples
swiftmacosaccessibilityvoiceover

How to Implement NSSlider Accessible for VoiceOver on MacOS?


I have a slider:NSSlider and valueLabel:NSTextField, and I'm wondering what's the proper way to make it accessible for VoiceOver users.

First I connected a send action for slider to sliderChanged function to update valueLabel.

valueLabel.stringValue = String(slider.integerValue)

VoiceOver reads the label correctly, but it reads the slider in percentage. To fix this, I changed sliderChanged function to setAccessibilityValueDescription.

slider.setAccessibilityValueDescription(String(slider.integerValue))

Now VoiceOver correctly reads the value for the slider. However, it sees both valueLabel and slider, so it's redundant.

I tried valueLabel.setAccessibilityElement(false), but VoiceOver doesn't seem to ignore.

Could someone advise what would be the proper way to implement this? Thanks!


Solution

  • The best way to do this is to create a custom "ContainerView" class (which inherits from UIView) that contains the label and the slider, make the ContainerView an accessibilityElement, and set its accessibilityTraits to "adjustable." By creating a ContainerView that holds both the valueLabel and the slider, you remove the redundancy that is present in your current implementation, while not affecting the layout or usability of the slider/valueLabel for a non-VoiceOver user. This answer is based on this video, so if something is unclear or you want more in-depth info, please watch the video!

    Setting a view's UIAccessibilityTraits to be "Adjustable" allows you to use its functions accessibilityIncrement and accessibilityDecrement, so that you can update whatever you need to (slider, textfield, etc). This trait allows any view to act like a typical adjustable (without having to add UIGestureRecognizers or additional VoiceOver announcements).

    I posted my code below for convenience, but it is heavily based on the video that I linked to above. (I personally am an iOS developer, so my Swift code is iOS-based)

    Note -- I had to override the "accessibilityValue" variable -- this was to make VoiceOver announce changes in the slider whenever the user swiped up or down.

    My ContainerView class contains the following code:

    class ContainerView: UIView {
    
        static let LABEL_TAG = 1
        static let SLIDER_TAG = 2
    
        var valueLabel: UILabel {
            return self.viewWithTag(ContainerView.LABEL_TAG) as! UILabel
        }
    
        var slider: UISlider {
            return self.viewWithTag(ContainerView.SLIDER_TAG) as! UISlider
        }
    
        override var accessibilityValue: String? {
            get { return valueLabel.text }
            set {}
        }
    
        override var isAccessibilityElement: Bool {
            get { return true }
            set { }
        }
    
        override var accessibilityTraits: UIAccessibilityTraits {
            get { return UIAccessibilityTraitAdjustable }
            set { }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            valueUpdated()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            valueUpdated()
        }
    
        func valueUpdated() {
            valueLabel.text = String(slider.value)
            slider.sendActions(for: .valueChanged)
        }
    
        override func accessibilityIncrement() {
            super.accessibilityIncrement()
    
            slider.setValue(slider.value + 1, animated: true)
            valueUpdated()
        }
    
        override func accessibilityDecrement() {
            super.accessibilityDecrement()
    
            slider.setValue(slider.value - 1, animated: true)
            valueUpdated()
        }
    }
    

    Hope this helps!