iOS Refresh Accessibility Label - ios

I have a button with accessibility label #"a". When the button is pressed, I have a callback that sets the accessibility label button.accessibilityLabel = #"b". I know this line of code runs. However, if I tap the button again, VoiceOver still reads a. Unfortunately, the code I'm working with is proprietary, so I can't share it directly.
However, in general, I would like to know what issues might cause VoiceOver to not recognize an update to a label.

THE BEST way to handle dynamic accessibility labels is to override the property functions on the views that are being focused (EX: on a UIButton). This allows TWO things. A: it's a lot easier to maintain than setting the property everywhere it can change. B: you can log information and see when the system is requesting that information, so you can better understand WHY things are happening. So even if it doesn't directly fix your issue, seeing WHEN the system requests your value, and logging that data, is inherently valuable.
Doing this in Objective C
#implementation YourUIButton
-(NSString*)accessibilityLabel {
if(someCondition) {
return #"a";
} else {
return #"b";
}
}
#end
In Swift
public class YourUIButton : UIButton
override public var accessibilityLabel: String? {
get {
if (someCondition) {
return "a"
} else {
return "b"
}
}
set {
NSException.raise(NSException("AccessibilityLabelException", "You should not set this accessibility label.", blah))
}
}
}
You could also use this logic JUST to debug, and allow setting and such.
There are a lot of potential issues here. Race conditions, which view is actually getting focus, is there some parent child relationship going on, etc. Overriding the property and adding logging statements to the above code will help you understand what view is actually getting the accessibility label requested and when. Super valuable information!

Use this while changing button text
UIAccessibility.post(notification: .layoutChanged, argument: yourButton)

Try to add UIAccessibilityTraitUpdatesFrequently to your buttons property accessibilityTraits
- (void)viewDidLoad {
myButton.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently
}
Also, when changing accessibilityLabel be sure that you're on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
myButton.accessibilityLabel = #"b";
});

You don't really need a way to refresh the voice over labels. Its done automatically. I have tried this and it works as expected.
class ViewController: UIViewController {
var tapCount = 0
var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
button = UIButton(type: .system)
button.setTitle("Hello", for: .normal)
button.frame = CGRect(x: 10, y: 10, width: 100, height: 50)
view.addSubview(button)
button.accessibilityLabel = "Hello Button"
button.accessibilityHint = "Tap here to start the action"
button.accessibilityIdentifier = "hello_button"
button.addTarget(self, action: #selector(buttonTap(sender:)), for: .touchUpInside)
}
#IBAction func buttonTap(sender:UIButton) {
tapCount = tapCount + 1
sender.accessibilityLabel = "Hello button, tapped \(tapCount) times"
}
}
What Voice oversays:
Hello Button-pause-Button-pause-Tap here to start the action
On button tap
Hello button, tapped one times
Another tap
Hello button, tapped two times

Related

Why use tags with UIButtons?

I'm studying Swift and I have a question.
When we make some button's Action function, sometimes tags are used.
After all to set button's tags with code, we must connect button's Outlet from storyboard.
Why use tags instead of outlet's variable name?
Any number of buttons can have a single action and we then need tag to distinguish action based on button tag. you don't actually need outlet for each button if you are setting tag from storyboard, Here is a detailed articles about tags:
Working With Multiple UIButtons and Utilizing Their Tag Property
Many cases many button have the same ibaction. In this situation , tag can help
As others have said the function could be completely independent of the button, it could be in another class altogether.
Buttons may not be in StoryBoards and could be created programatically, potentially dynamically, so there maybe no case of checking if the button passed to the function is the same as a local variable (as a button may not be an ivar on the class the function is based on)
Tags give an easy way to provide some identifier to button or view that can be cross referenced when the function is called in order to achieve the desired out come
Here's an example to illustrate, bear in mind this is somewhat contrived in order to provide an detail
In you ViewController you need to dynamically create a button but don't want to store it as a variable on the VC. You also have a separate class which is handling taps
We could create an enum to handle button types, this handles the tag and title for the button type
enum ButtonType: String {
case nonIvar
case other
var tag: Int {
switch self {
case .nonIvar:
return 0
case .other:
return 1
}
}
var title: String {
switch self {
case .nonIvar:
return "Non iVar Button"
case .other:
return "Some other Button"
}
}
}
we then have our class that handles the tap function
class ButtonHandler {
#IBAction func handleTap(_ sender: UIButton) {
if sender.tag == ButtonType.nonIvar.tag {
// do something with the none ivar buton
} else {
// handle other type
}
}
}
then in our view controller we create our button hooking the two instances above up
let handler = ButtonHandler()
func addMyButton() {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
let buttonType: ButtonType = .nonIvar
myButton.setTitle(buttonType.title, for: .normal)
myButton.tag = buttonType.tag
myButton.addTarget(self, action: #selector(handler.handleTap(_:)), for: .touchUpInside)
view.addSubview(myButton)
}
This means that the handler can be used on any class and the button types could be equally used throughout the app and the behaviour would be the same without duplicating code
Hope this helps!

Weird UIButton behaviour in Today Widget

I added a today extension to my app. I edited the vanilla widget a little bit and made it look like this:
Please notice the UIButton titled "Went 1st". When it gets pressed (touched up inside to be exact), it triggers this action:
#IBAction func coinChanged(sender: UIButton) {
if sender.titleLabel?.text == "Went 1st" {
coin = true
sender.titleLabel!.text = "Went 2nd"
} else {
coin = false
sender.titleLabel!.text = "Went 1st"
}
}
It basically alters between two states, changing its title and a variable accordingly.
Here is the problem though - when I press it, it indeed changes its title, but immediately changes it back, ending up on the same title as it had initially. My first thought was the action gets called twice after a press, but when I checked with print I found out it gets called only once. Sometimes the prints didn't even show up in console, but that's a different story.
So, that's one problem. There's one more, though - when I press the button, the whole widget gets misplaced. To know what I mean, look at the first picture (that's the widget before any presses) and now on this one (after the button gets pressed):
You can see that the borders are now on the very edge of TodayView. For reference, here are the constraints of the first segmented control:
Edit: Here are the constraints for "Went 1st/2nd" button:
Edit 2: Be sure to tell me what's wrong if you downvote, so I can avoid making the same mistakes next time
The problem is you should not be setting the button text like that. The title label is primarily used to set text size, font, color, etc. To set the title use something like this:
sender.setTitle("Button Title", forState: UIControlState.Normal)
So the new ib action should look like:
#IBAction func coinChanged(sender: UIButton) {
if sender.titleLabel?.text == "Went 1st" {
coin = true
sender.setTitle("Went 2nd", forState: UIControlState.Normal)
} else {
coin = false
sender.setTitle("Went 1st", forState: UIControlState.Normal)
}
}

Touch Up Inside not working properly

I have an app with some buttons, when those buttons are pressed the image on them should change. I assume that the TouchUpInside runs when you tap and remove the finger while still holding inside the area of the element, however it only works rarely and I'm not sure why.
The reason I use TouchUpInside instead of TouchDown is because I want the user to be able to cancel the action.
I'm sorry if I've misunderstood anything about those events and if this has already been asked. I couldn't find an answer to my problem searching the web.
//The IBAction is set to trigger on TouchUpInside
#IBAction func action11(sender: UIButton) {
setTile(sender)
}
func setTile(sender: UIButton) {
if turn {
print("O's turn")
sender.setImage(xTile, forState: .Normal)
turn = false
}
}
EDIT: Added the necessary code
There are some properties of UIButtons which you can use to achieve what you want.
You can use Default and selected state of uibutton to set two different images.
In XIB select state "Default" and assign default image to that state again select state to "Selected" and assign image which you want after button section.
and add following line in button selection method.
-(IBAction)buttonTapped:(UIButton *)sender{
sender.selected = !sender.selected;
}
Your understanding is correct, you need to use touchUpInside.
I assume you are trying to create a button that has a toggle function. On one touch you want the button to have the value Say "X" and when touched again the button has a value "O".
Take a look at this code below, this should do the job.
class ViewController: UIViewController {
var isButtonPressed = false{
// Adding a Property Observer, that reacts to changes in button state
didSet{
if isButtonPressed{
// Set the Value to X.
}else{
// Set the Value to O.
}
}
}
#IBAction func changeButtonValue(sender: UIButton) {
// Toggle the button value.
isButtonPressed = !isButtonPressed
}
}
If you don't set turn=true after the first time, this code is executed it will be executed only one.
if turn {
print("O's turn")
sender.setImage(xTile, forState: .Normal)
turn = false
}
Check if the button frame is large enough to get finger touch.
Apple says at least 35x35 pixel.

UIView does not have a member named, "inertia"

I am attempting to create a button programmatically that will fade out when tapped, change the text, then fade back in. However, when i try to write the code for the fade animation, I get the error, "'UIView' does not have a member named 'inertia' Inertia is the name of the button, of course.
Here is my code for creating the button, it is within a function that is called within the viewDidLoad() function:
var inertia = UIButton.buttonWithType(UIButtonType.System) as UIButton
inertia.frame = CGRectMake(firstView.frame.width/2-(inertia.frame.width/2), firstView.frame.height/10, firstView.frame.width, firstView.frame.height/10)
inertia.frame = CGRectMake(firstView.frame.width/2-(inertia.frame.width/2), firstView.frame.height/10, firstView.frame.width, firstView.frame.height/10)
inertia.setTitle("Newton's First Law of Motion", forState: UIControlState.Normal)
inertia.addTarget(self, action: "tapped:", forControlEvents: .TouchUpInside)
self.firstView.addSubview(inertia)
This is my line of code for when the button is tapped so far, where the error occurs:
UIView.animateWithDuration(0.4, animations: {self.firstView.inertia.alpha = 0})
I believe I am missing something in the button creation, because when I create an outlet for a button from the storyboard the fade animation will not produce an error.
Please help me fix this issue as I will be using this often. Also if you could find a way for me to create the button's frame properly without having to declare it twice that would be helpful as well.
What I have tried (to no avail):
I have put : UIButton! after var inertia
I have tried `UIView.animateWithduration(0.4, animations: {self.inertia.alpha = 0})
Just to break down the problem line, specifically self.firstView.inertia.alpha. Self is obviously whatever class instance you are in. I assume from the error you are reporting and your code that firstView is a UIView. Now you have added inertia as a subview of firstView, but that does not create a property named inertia on the view. In other words, there is no firstView.inertia. What firstView has is a subviews property which is an array of anyobject.
In other words the button that you created and called inertia is now somewhere in the array firstView.subviews, though it's hard to say where depending on how many other views it has as subviews.
You could iterate through the subviews array to find the button which you'd previously called inertia, but it might be simpler to just keep a reference to inertia. You could make it a property in your class (if there is only one such button) and the call your code with
UIView.animateWithDuration(0.4, animations: {self.inertia.alpha = 0})
As I understand from your description you code looks something like this:
class ViewController:UIViewController {
var firstView:UIView!
func tapped(button: UIButton) {
UIView.animateWithDuration(0.4) { self.firstView.inertia.alpha = 0 }
}
override func viewDidLoad() {
super.viewDidLoad()
var inertia = UIButton.buttonWithType(UIButtonType.System) as UIButton
inertia.frame = CGRectMake(firstView.frame.width/2-(inertia.frame.width/2), firstView.frame.height/10, firstView.frame.width, firstView.frame.height/10)
inertia.frame = CGRectMake(firstView.frame.width/2-(inertia.frame.width/2), firstView.frame.height/10, firstView.frame.width, firstView.frame.height/10)
inertia.setTitle("Newton's First Law of Motion", forState: UIControlState.Normal)
inertia.addTarget(self, action: "tapped:", forControlEvents: .TouchUpInside)
self.firstView.addSubview(inertia)
}
}
Now, you get that error because inerta is not declared as a property in your class, you only create it in a function. If you want to use that button when pressed in tapped() then simply change:
UIView.animateWithDuration(0.4) { self.firstView.inertia.alpha = 0 }
// To
UIView.animateWithDuration(0.4) { button.alpha = 0 } // When TouchUpInside the button will pass itself
Or you can make it a propery by adding:
var inertia:UIButton! // Under class ...
And remove var in viewDidLoad when setting it.

What's the simplest way to receive tap events on a disabled UIButton?

I have a UIButton on a form, and want to put it in a disabled state when the form is incomplete. However, I still want to be able to detect if a user attempts to press the button even in its disabled state so that the interface can let the user know that certain required fields on the form are not filled-in yet (and perhaps scroll to that field and point it out, etc.).
There doesn't seem to be any straightforward way to do this. I tried simply attaching a UITapGestureRecognizer to the UIButton but it doesn't respond when the button is in a disabled state.
I'd like to avoid subclassing UIButton if possible, unless it's the only way.
Create a fallback button. Put it behind the main button. Set its background and text colors to [UIColor clearColor] to ensure it won't show up. (You can't just set its alpha to 0 because that makes it ignore touches.) In Interface Builder, the fallback button should be above the main button in the list of subviews, like this:
Give it the same frame as the main button. If you're using autolayout, select both the main and fallback buttons and create constraints to keep all four edges equal.
When the main button is disabled, touches will pass through to the fallback button. When the main button is enabled, it will catch all the touches and the fallback button won't receive any.
Connect the fallback button to an action so you can detect when it's tapped.
Based on #rob idea, I sub-class a UIButton, and add a transparent button before someone addSubview on this button.
This custom UIButton will save many time about adjusting the UI components on the storyboard.
Update 2018/08
It works well, and add some enhanced detail to this sub-class. I have used it for 2 years.
class TTButton : UIButton {
// MARK: -
private lazy var fakeButton : UIButton! = self.initFakeButton()
private func initFakeButton() -> UIButton {
let btn = UIButton(frame: self.frame)
btn.backgroundColor = UIColor.clearColor()
btn.addTarget(self, action: #selector(self.handleDisabledTouchEvent), forControlEvents: UIControlEvents.TouchUpInside)
return btn
}
// Respect this property for `fakeButton` and `self` buttons
override var isUserInteractionEnabled: Bool {
didSet {
self.fakeButton.isUserInteractionEnabled = isUserInteractionEnabled
}
}
override func layoutSubviews() {
super.layoutSubviews()
// NOTE: `fakeButton` and `self` has the same `superView`.
self.fakeButton.frame = self.frame
}
override func willMoveToSuperview(newSuperview: UIView?) {
//1. newSuperView add `fakeButton` first.
if (newSuperview != nil) {
newSuperview!.addSubview(self.fakeButton)
} else {
self.fakeButton.removeFromSuperview()
}
//2. Then, newSuperView add `self` second.
super.willMoveToSuperview(newSuperview)
}
#objc private func handleDisabledTouchEvent() {
//NSLog("handle disabled touch event. Enabled: \(self.enabled)")
self.sendActionsForControlEvents(.TouchUpInside)
}
}
You have a great misunderstanding of user experience.
If a button is disabled, it is meant to be non-interactable.
You can not click on a disabled button, that is why it is disabled.
If you want to warn users about something when that button is clicked (e.g. form not filled correctly or completely), you need to make that button enabled. And just warn users when they click on it, instead of proceeding further with app logic.
Or you can keep that button disabled until form criteria are met, but show what is wrong with the form using another way, like putting exclamation marks near text fields, changing text field colors to red, or something like that...
But never try to add gesture recognizers, or hidden fallback buttons to a disabled button.
Check those and let me know if you see a disabled button:
https://airbnb.com/signup_login
https://spotify.com/us/signup/
https://netflix.com/signup/regform
https://reddit.com/register/
https://twitter.com/signup
https://facebook.com/r.php
https://appleid.apple.com/account
https://accounts.google.com/SignUp
https://login.yahoo.com/account/create
https://signup.live.com/signup
All the proceed buttons on these websites are always enabled, and you get feedback about what is wrong when you try to continue.
And here is really good answer: https://ux.stackexchange.com/a/76306
Long story short: disabled UI elements meant to be not-interactable.
Trying to make them interactable while they are disabled is the same to making them enabled in the first place.
So, for your question's case, it is just a styling issue. Just try styling your button, instead of making it disabled/enabled.

Resources