chickenparm
chickenparm

Reputation: 1630

Accessibility Increment and Decrement not called for UISlider

I'm trying to make my app more accessible for Voice Over users. I have a slider that has numbers 1-100. If a user with Voice Over turned on swipes up or down to change the value, several numbers are being skipped. This means that an exact number is not able to be set. I'm following the suggestion from this site on subclassing UISlider and overriding accessibilityIncrement() and accessibilityDecrement() but they do not get called when the slider value changes. Below is my subclassed slider. Any idea why the methods are not getting called?

class FontSizeSlider: UISlider {

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.isAccessibilityElement = true
    self.accessibilityTraits.insert(.adjustable)
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
  }

  override func accessibilityIncrement() {
    self.value += 1
    self.sendActions(for: .valueChanged)
  }

  override func accessibilityDecrement() {
    self.value -= 1
    self.sendActions(for: .valueChanged)
  }
}

Upvotes: 1

Views: 1815

Answers (1)

Adrian
Adrian

Reputation: 16735

This is something I need to know for work, so this was a fantastic exercise for me. Thank you for posting the question. Anyway, I got it to work after taking a peek at this page on Apple's website.

I could not get the increment/decrement methods to be called, either. I suspect they're stepper-specific. The value property, OTOH, gets called.

Here's the code I came up with to get it to work:

class FontSizeSlider: UISlider {

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    func setup() {
        isAccessibilityElement = true
        accessibilityLabel = "Font Size Slider"
        accessibilityIdentifier = "fontSizeSlider"
        // accessibilityIdentifier = AccessibilityConstants.fontSizeSlider.rawValue

        minimumValue = 0
        maximumValue = 100
        isContinuous = true
    }

    override var accessibilityValue: String? {
        get {
            return sliderValueString
        }

        set {
            super.accessibilityValue = sliderValueString
        }
    }

    override var accessibilityTraits: UIAccessibilityTraits {
        get {
            return .adjustable
        }

        set {
            super.accessibilityTraits = newValue
        }
    }

    // Nobody needs to know about this outside the class, so marked it private
    private var sliderValueString: String {
        let stringValue = String(Int(value))

        return "The font size is \(stringValue)"
    }
}

You'll notice I used the setup() method, which does the same stuff for both initializers. You can tweak your values as you see fit for the min/max values.

You'll note I added accessibilityLabel, so it doesn't read off that it's a generic slider. I added the accessibilityIdentifier in there, too. That's something that can be used for UI tests so the element can be identified.

You'll probably want to put the accessibilityIdentifier somewhere where "everyone" can see it. Perhaps an enum. Here's what the enum implementation would look like:

enum AccessibilityConstants: String {
    case fontSizeSlider
}

// Usage
accessibilityIdentifier = AccessibilityConstants.fontSizeSlider.rawValue

I overrode the accessibilityValue with a custom setter and getter. Additionally, I created a computed var for the string that's read off when the accessibilityValue is updated. Here's the code for that portion of it. Note I made it private because nobody outside the class needs to know about it:

// I adapted this from Apple's accessibility page that I posted above
override var accessibilityValue: String? {
    get {
        return sliderValueString
    }

    set {
        super.accessibilityValue = sliderValueString
    }
}


private var sliderValueString: String {

    let stringValue = String(Int(value))

    return "The font size is \(stringValue)"
}

One last thing...you don't need self everywhere unless you're accessing a property of your custom UISlider inside a closure like an animation block or a completion block.

Update

Deleted...

Update 2

So let's say you're on your viewController, you could add a target action to the slider, like so:

    slider.addTarget(self, action: #selector(doSomething), for: .valueChanged)

@objc func doSomething() {
    print("How much wood could a wood chuck chuck if a wood chuck could chuck wood")
}

Whenever value changes, your selector will get called.

Upvotes: 2

Related Questions