Accessibility Increment and Decrement not called for UISlider - ios

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)
}
}

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.

Related

UILabel subclass not showing when using constraints

I've created a subclass to manage my Theme but is not showing neither on device or simulator.
Here my Header.swift:
import Foundation
import UIKit
class Header: UILabel {
override var textColor: UIColor! {
// White Color
get { return ThemeManager.currentTheme.palette.primary }
set {}
}
override var font: UIFont! {
get { return ThemeManager.currentTheme.textStyle.headerText }
set {}
}
}
Here the implementation: (inside the viewcontroller)
var titleLabel: Header = Header()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Background Image over the view
setupBackground()
setupStartButton()
setupTitleLabel()
print(titleLabel.frame)
}
// MARK: - Header
private func setupTitleLabel() {
titleLabel.text = "0.0m"
// titleLabel.font = ThemeManager.currentTheme.textStyle.headerText
// titleLabel.textColor = ThemeManager.currentTheme.palette.primary
view.addSubview(titleLabel)
view.bringSubviewToFront(titleLabel)
setupTitleLabelAutolayout()
}
private func setupTitleLabelAutolayout() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
But if I use UILabel instead of Header it works perfectly as expected.
I've also tried to implement init?(coder aDecoder: NSCoder) and init(frame: CGRect) but nothing changed.
If I set a frame on init then shows the text, but not styled and ignoring my constraints.
Surely I'm missing something, but what?
To avoid usefulness answers, here some infos:
The UILabel textColor is white
The background is black and has an image over it.
I've tried to remove the image and all the stuff around except for the label and nothing changed.
That's a poor reason to use subclassing. It doesn't allow you to mix-and-match when appropriate.
Better would be to make an extension:
extension UILabel {
func withHeaderStyle() -> UILabel {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
return self
}
}
Then at point of use:
var titleLabel = UILabel().withHeaderStyle()
You can make several of these "withXStyle" methods and at the point of use you can chain them together. That's something you can't do with inheritance.
In general you should only use inherence when you want to change behavior. It's ill suited for changing data.
I've fixed that by editing the Header to this:
import Foundation
import UIKit
class Header: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupStyle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupStyle()
}
private func setupStyle() {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
}
}
Basically if I understood right, when I set the getter in the label it doesn't (if you think about, it's quite obvious) anything.
I still think that there are better solutions, but this works fine for me so, I'll keep that.
Now you may ask: "Why did you overwritten the getter instead of doing this?"
It's the right question, and the right answer is that I read it in a swift article on medium, so I tought it was right.
PS: I've also tried with didSet but it obviously loop through it self and crash.

UIStackView subclass with static spacing

I am creating a subclass of UIStackView, and I want spacing to be static and non-overwritable. I thought this would be simply done by overriding the spacing property:
class MyStackView: UIStackView {
override var spacing: CGFloat {
get { return 8.0 }
set {}
}
}
This, however, does not work. The getter is never called and no spacing is displayed. The only solution I found for this is the following piece of code:
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
super.spacing = 8.0
}
override var spacing: CGFloat {
get { return 8.0 }
set {}
}
}
It actually doesn't matter what I return in get, it's never called by anything anyway.
Can anybody explain this behavious]r and how to avoid it?
In my opinion here what's happening. You want to achieve two things:
Spacing at MyStackView should be 8.0 by default
That spacing value should not be changeable from outside
To achieve the first thing you need to override the initializers and set your default value inside on them.
class MyStackView: UIStackView {
private let defaultSpacing: CGFloat = 8.0
override init(frame: CGRect) {
super.init(frame: frame)
super.spacing = defaultSpacing
}
required init(coder: NSCoder) {
super.init(coder: coder)
super.spacing = defaultSpacing
}
}
For the second thing, you need to override the getter and setter of spacing to get the control on any changing from the outside.
class MyStackView: UIStackView {
...
override var spacing: CGFloat {
get { return defaultSpacing }
set { print("MyStackView spacing it always \(defaultSpacing)") }
}
}
Why can't you simply override a variable and get the same result?
UIStackView.spacing property is computed, it's not a container. We might not care how does original getter works because we know that our spacing is always the same. We know its value and can always return it at overriden getter.
But we also don't know how does original setter works and what changes does it make in UIStackView. Since that, we can't simply override getter. We have to explicitly call super.spacing = defaultValue once, to trigger original setter and set the desired value.

How can I make an arbitrary UIAccessibilityElement behave like a UISwitch for VoiceOver?

When selecting a native switch with VoiceOver, the announcement will contain "Off" or "On" with an additional hint "double tap to toggle setting".
I have tried using the accessibility trait UIAccessibilityTraitSelected, but that only results in "Selected" being announced, with no hint unless I provide one explicitly.
Using the Accessibility Inspector I've also noticed that native UIKit switches have an accessibilityValue of 1 when enabled, but providing that does not change VoiceOver behavior.
- (UIAccessibilityTraits)accessibilityTraits {
if (toggled) {
return UIAccessibilityTraitSelected;
} else {
return UIAccessibilityTraitNone;
}
}
- (NSString*)accessibilityValue {
if (toggled) {
return #"1";
} else {
return #"0"
}
}
Is it possible to provide some combination of traits/value/label such that TalkBack recognizes this element as a Switch, without using a UISwitch?
I have created an accessible view that acts like a switch here.
The only way that I have been able to get any arbitrary element to act like a Switch is when inheriting the UIAccessibilityTraits of a Switch. This causes VoiceOver to read the Accessibility Value (0 or 1) as "Off" or "On," adds the hint "Double tap to toggle setting", and makes VoiceOver say "Switch Button."
You could potentially do this by overriding the view's Accessibility Traits like so:
override var accessibilityTraits(): UIAccessibilityTraits {
get { return UISwitch().accessibilityTraits }
set {}
}
Hope this helps!
You can create a custom accessibility element behaving like a UISwitchControl with whatever you want.
The only thing to be specified is the way VoiceOver should interpret it.
Let's suppose you want to gather a label and a view to be seen as a switch control.
First of all, create a class for grouping these elements into a single one :
class WrapView: UIView {
static let defaultValue = "on"
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
convenience init(with label: UILabel,and view: UIView) {
let viewFrame = label.frame.union(view.frame)
self.init(frame: viewFrame)
self.isAccessibilityElement = true
self.accessibilityLabel = label.accessibilityLabel
self.accessibilityValue = WrapView.defaultValue
self.accessibilityHint = "element is" + self.accessibilityValue! + ", tap twice to change the status."
}
}
Then, just create your custom view in your viewDidAppear() :
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
#IBOutlet weak var myLabel: UILabel!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let myCustomView = WrapView.init(with: myLabel, and: myView)
self.view.addSubview(myCustomView)
}
}
Finally, to have a custom view behaving like a switch control, just override the accessibilityActivate function in your WrapView class to implement your logic when your view is double tapped :
override func accessibilityActivate() -> Bool {
self.accessibilityValue = (self.accessibilityValue == WrapView.defaultValue) ? "off" : "on"
self.accessibilityHint = "element is" + self.accessibilityValue! + ", tap twice to change the status."
return true
}
And now you have a custom element that contains whatever you want and that behaves like a switch control for blind people using VoiceOver without using a UISwitch as you wanted.

Value of #IBInspectable set in storyboard not propagated to the code

Without any experience with iOS, I have been asked to upgrade to Swift 4 an application that is using Swift 3. I updated the language to use in "Swift language version" and I followed the recommendations made by xCode (explicitly use #objc).
Once all errors fixed the application works, but one UI-related feature is not working anymore. The app has a custom UITextField that is used to enter a password. Depending on the value of a #IBInspectable class field of the custom text field the text is hidden or not.
The value of this variable is correctly set in MainStoryboard but when the class is instantiated this value is not set to the value in the storyboard, and as a result, the custom text field does not behave how it should. The log does not mention any error.
Does anybody experience the same problem?
I checked storyboard and class, I did not find any problems, I also compared the code of the version with Swift 3 and the one I updated with Swift 4 and there are no differences (except the #objc ones).
Are there something else to look that can help me to find why the value set in MainStoryboard is not propagated to the class?
Edit
Below is the code involved in the problem:
a variable isPassword is declared to reflect the value of the IBInspectable variable
the variable IBInspectable is declared
when awakeFromNib depending on the value of isPassword a method to either hide the text or not is called
#IBDesignable class CustomEditText: UITextField {
var isPassword = false
// Inspector variable determining if the text must be hidden or not
#IBInspectable var isAPasswordField: Bool {
get{
return self.isPassword
}
set{
self.isPassword = isAPasswordField
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setup() // Set borders, font, ...
}
override func awakeFromNib() {
super.awakeFromNib()
if isPassword { // isPassword does not reflect the value of isAPasswordField set in storyboard
self.isSecureTextEntry = true
setRevealButton() // the text is hidden
} else {
setCustomClearButton() // the text is not hidden
}
}
...
Answer
#Alladinian gave the good answer.
Below for completeness, another solution that I found:
#IBInspectable var isPasswordField: Bool = false {
didSet{
self.isPassword = isPasswordField
if isPassword {
self.isSecureTextEntry = true
setRevealButton()
} else {
setCustomClearButton()
}
}
}
The correct form would be (note the setter assignment):
#IBInspectable var isAPasswordField: Bool {
get {
return self.isPassword
}
set {
self.isPassword = newValue
}
}
since the actual value have not been (and will not be - at least 'directly') set on isAPasswordField (remember, we are implementing the setter of a computed property after all...)
Also, why don't you just use isPassword directly by marking it as inspectable, avoiding the need for an extra ivar?
PS: newValue is the default name for the to-be-set value. You can read more about it under Shorthand Setter Declaration section in the Swift Programming Language book
The following code works fine for me for newer versions:
#IBInspectable var isAPass: Bool {
get {
return self.isSecureTextEntry
}
set {
self.isSecureTextEntry = newValue
}
}

Set default appearance for a UIView subclass without overriding initialize() in Swift 3.1

I have a subclass of UITextView, which needs to have a specific default appearance. So far, I've been able to achieve this by overriding the initialize() class function, which has been deprecated in Swift 3.1.
public class CustomTextView : UITextView {
override public class func initialize() {
self.appearance().backgroundColor = .green
}
}
Is there a way to achieve the same thing pure Swift?
I'm working around the loss of the class method initialize by using a construct like this:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
This construct has the advantage that doInitialize will be initialized only once, so the code connected with it will run only once (in this case we'll configure the appearance proxy only once); and it is early enough to affect even the first instance created (that is, every CustomTextView you ever make will in fact be green).

Resources