Setting UIView.isAccessibleElement to true disables clicking of subview in voiceovermode - ios

So I have a custom view controller that displays a dialog with a couple of buttons.
When the view appears I want voiceover to read out some basic information describing the dialog.
To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
My problems now is that the buttons are not clickable directly.
They must be reached only by swiping right on the screen.
class MyViewController: UIViewController {
let parent = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parent.addSubview(button1)
parent.addSubview(button2)
parent.isAccessibilityElement = true
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
parent.accessibilityLabel = "Message"
self.view.addSubview(parent)
self.view.accessibilityElements = [parent, button1, button2]
}
override func viewDidAppear(_ animated: Bool) {
}
}
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Also, the view needs to be a modal so that focus is trapped on the view.

To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
That's definitely the problem: you can't have the parent view and its children accessible all together ⟹ see the example sheet of this explanation.
If a parent view is accessible, its children won't be seen by VoiceOver and conversely.
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Using VoiceOver, you must be as accurate and brief as possible.
The description of a view is provided by its elements when you explore the screen or by its title itself : in my view, you shouldn't read out a description that a perfect title should provide in addition to the correct implementation of the different components of your page.
There's a great presentation made by a blind person who explains how to write labels inside an app to be well understood.
Also, the view needs to be a modal so that focus is trapped on the view.
The best way to reach this purpose is to use the accessibilityViewIsModal property of your view ⟹ take a look at this example introduced during a WWDC session if need be.

You can post a notification with a message as parameter, so you would not need to set the parent view as an accessibility element. This would solve both of your problems.
Example code:
let parentVc = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parentVc.addSubview(button1)
parentVc.addSubview(button2)
button1.setTitle("btn1", for: .normal)
button2.setTitle("btn2", for: .normal)
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
self.view.addSubview(parentVc)
self.view.accessibilityElements = [button1, button2]
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: "Message here");
}

Related

How to disable backBarButtonItem? [duplicate]

Is there any official way how to set UIBarButtonItem.enabled property? I tried to set a backButtonItem in previous controller. But enabled property is ignored.
More in this simple example project.
I don't want to some solution like "make your own leftBarButtonItem and set its alpha ..."
Edit: I don't want to hide it, only disable it with dimmed colour and disabled user interaction. It's exactly the same behaviour as for disabled leftBarButtonItem.
As of today it is not possible to disable the back button using the enabled property. The backBarButtonItem property will be nil unless you create a custom item and even then it will ignore the enabled property. There are a couple (non-satisfactory) ways around this.
Hide the button
This is what Apple wants you to do given that they ignore the enabled property. It is as simple as
navigationItem.hidesBackButton = true
and should be the preferred approach unless you have good reasons.
Disable and Tint the Navigation Bar
You can disable user interaction on the whole navigation bar and tint it to make the back button appear disabled.
navigationController?.navigationBar.isUserInteractionEnabled = false
navigationController?.navigationBar.tintColor = UIColor.lightGray
This does, unfortunately, affect other elements in the navigation bar as well so it might not be an option if, for instance, you have another bar button item on the right side.
Use a Custom Left Bar Button Item
The leftBarButtonItem does not ignore the enabled property so you could create a custom item and trigger the pop manually when it is activated.
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ThisClass.backButtonTapped))
...
navigationItem.leftBarButtonItem?.isEnabled = false
func backButtonTapped() {
self.navigationController?.popViewController(animated: true)
}
This will, however, not have the back bar button style with the leading triangular indicator.
Add below code in your ViewController2.swift Class.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true;
}
It will hide your back button.
If you want to hide it, UInavigationItem has a hidesBackButton property.
I know this is an old thread, but this may help someone else.
As mentioned by hennes, you can no longer disable the back button. Instead, you will need to disable the entire navigationBar.
The approach I took, was disabling the navigationBar, and then applying an 0.5 alpha to the subviews of the navigation bar.
In your ViewController class:
func changeBarButtons(alpha: CGFloat) {
navigationController?.navigationBar.subviews.forEach { firstViews in
firstViews.subviews.forEach { view in
if ["_UIButtonBarButton", "_UIButtonBarStackView"].contains(type(of: view).description()) {
view.alpha = alpha
}
}
}
}
func set(loading: Bool) {
let alpha: CGFloat = loading ? 0.5 : 1
navigationController?.navigationBar.isUserInteractionEnabled = !loading
changeBarButtons(alpha: alpha)
}
Keep in mind, that Apple could change the names of the class any time. That being said, it's highly unlikely they do so. If you don't mind the title of the View Controller fading out, you can apply the alpha to all the subviews, without checking the class name.
Don't try to disable your custom back button (won't work), just set a new one which is disabled. You can reach the previous navigation item through the UINavigationBar.backItem property.
// set disabled back button
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItem.Style.plain, target: nil, action: nil)
backButton.isEnabled = false
navigationController?.navigationBar.backItem?.backBarButtonItem = backButton
// disable pop gesture
navigationController?.interactivePopGestureRecognizer?.isEnabled = false

How to disable back button in navigation bar

Is there any official way how to set UIBarButtonItem.enabled property? I tried to set a backButtonItem in previous controller. But enabled property is ignored.
More in this simple example project.
I don't want to some solution like "make your own leftBarButtonItem and set its alpha ..."
Edit: I don't want to hide it, only disable it with dimmed colour and disabled user interaction. It's exactly the same behaviour as for disabled leftBarButtonItem.
As of today it is not possible to disable the back button using the enabled property. The backBarButtonItem property will be nil unless you create a custom item and even then it will ignore the enabled property. There are a couple (non-satisfactory) ways around this.
Hide the button
This is what Apple wants you to do given that they ignore the enabled property. It is as simple as
navigationItem.hidesBackButton = true
and should be the preferred approach unless you have good reasons.
Disable and Tint the Navigation Bar
You can disable user interaction on the whole navigation bar and tint it to make the back button appear disabled.
navigationController?.navigationBar.isUserInteractionEnabled = false
navigationController?.navigationBar.tintColor = UIColor.lightGray
This does, unfortunately, affect other elements in the navigation bar as well so it might not be an option if, for instance, you have another bar button item on the right side.
Use a Custom Left Bar Button Item
The leftBarButtonItem does not ignore the enabled property so you could create a custom item and trigger the pop manually when it is activated.
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ThisClass.backButtonTapped))
...
navigationItem.leftBarButtonItem?.isEnabled = false
func backButtonTapped() {
self.navigationController?.popViewController(animated: true)
}
This will, however, not have the back bar button style with the leading triangular indicator.
Add below code in your ViewController2.swift Class.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true;
}
It will hide your back button.
If you want to hide it, UInavigationItem has a hidesBackButton property.
I know this is an old thread, but this may help someone else.
As mentioned by hennes, you can no longer disable the back button. Instead, you will need to disable the entire navigationBar.
The approach I took, was disabling the navigationBar, and then applying an 0.5 alpha to the subviews of the navigation bar.
In your ViewController class:
func changeBarButtons(alpha: CGFloat) {
navigationController?.navigationBar.subviews.forEach { firstViews in
firstViews.subviews.forEach { view in
if ["_UIButtonBarButton", "_UIButtonBarStackView"].contains(type(of: view).description()) {
view.alpha = alpha
}
}
}
}
func set(loading: Bool) {
let alpha: CGFloat = loading ? 0.5 : 1
navigationController?.navigationBar.isUserInteractionEnabled = !loading
changeBarButtons(alpha: alpha)
}
Keep in mind, that Apple could change the names of the class any time. That being said, it's highly unlikely they do so. If you don't mind the title of the View Controller fading out, you can apply the alpha to all the subviews, without checking the class name.
Don't try to disable your custom back button (won't work), just set a new one which is disabled. You can reach the previous navigation item through the UINavigationBar.backItem property.
// set disabled back button
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItem.Style.plain, target: nil, action: nil)
backButton.isEnabled = false
navigationController?.navigationBar.backItem?.backBarButtonItem = backButton
// disable pop gesture
navigationController?.interactivePopGestureRecognizer?.isEnabled = false

Creating a floating menu in an iOS application

Looking to create a floating menu in Swift for an iOS application I am developing. Something along the lines of the little red circle menu as shown in the following image.
My initial thoughts were to extend the UIViewController class and add the respective drawing/logic there, however, the application is comprised of a few other controllers, more specifically the UITableViewController which in itself extends UIViewController. Is there perhaps a good place for an extension perhaps? Or is there a more eloquent way of drawing the menu on specific views without the mass duplication of menu related code?
The menu itself will be shown on most screens, so I need to selectively enable it. It'll also be somewhat contextual based on the view/screen the user is currently on.
Any awesome ideas?
You can create your own with the animations and all the things, or you can check this library
https://github.com/lourenco-marinho/ActionButton
var actionButton: ActionButton!
override func viewDidLoad() {
super.viewDidLoad()
let twitterImage = UIImage(named: "twitter_icon.png")!
let plusImage = UIImage(named: "googleplus_icon.png")!
let twitter = ActionButtonItem(title: "Twitter", image: twitterImage)
twitter.action = { item in println("Twitter...") }
let google = ActionButtonItem(title: "Google Plus", image: plusImage)
google.action = { item in println("Google Plus...") }
actionButton = ActionButton(attachedToView: self.view, items: [twitter, google])
actionButton.action = { button in button.toggleMenu() }
}
There is another alternative with this great library :
https://github.com/yoavlt/LiquidFloatingActionButton
You just have to implement the delegate and the dataSource in your ViewController:
let floatingActionButton = LiquidFloatingActionButton(frame: floatingFrame)
floatingActionButton.dataSource = self
floatingActionButton.delegate = self
You could use view controller containment. The menu can be its own view controller with its view laid transparently over top the content view controller.
For example this can be set up in the storyboard by dragging out two container views into a vanilla view controller.

UITableView and UIRefreshControl being moved down for unknown reason

I have a UITableViewController in my app with a UIRefreshControl added to it. Sometimes however (I'm not sure how to reproduce this, it happens every now and then), I get some extra whitespace at the top of the table view with the refresh control being offset even below that.
This is what it looks like (idle on the left, being pulled down on the right):
I don't have any clue what could be causing this. In my viewdidload I'm only instantiating the refresh control and calling an update function that sets the attributed title. I've moved adding the refresh control to the table view into the viewDidAppear as I've read elsewhere. This is what that code looks like:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
updateData()
}
override func viewDidAppear(animated: Bool) {
refreshControl!.addTarget(self, action: "updateData", forControlEvents: UIControlEvents.ValueChanged)
tableView.insertSubview(self.refreshControl!, atIndex: 0)
}
func updateData() {
//...
ServerController.sendParkinglotDataRequest() {
(sections, plotList, updateError) in
//...
// Reload the tableView on the main thread, otherwise it will only update once the user interacts with it
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
// Update the displayed "Last update: " time in the UIRefreshControl
let formatter = NSDateFormatter()
formatter.dateFormat = "dd.MM. HH:mm"
let updateString = NSLocalizedString("LAST_UPDATE", comment: "Last update:")
let title = "\(updateString) \(formatter.stringFromDate(NSDate()))"
let attributedTitle = NSAttributedString(string: title, attributes: nil)
self.refreshControl!.attributedTitle = attributedTitle
})
}
}
Do you need to add the refresh control as a subview of the tableView? I think all you need to do is assign self.refreshControl. According to the documentation:
The default value of this property is nil.
Assigning a refresh control to this property adds the control to the
view controller’s associated interface. You do not need to set the
frame of the refresh control before associating it with the view
controller. The view controller updates the control’s height and width
and sets its position appropriately.
Adding a subview in viewDidAppear could get executed more than once. If you push a controller from a cell and pop back this will get called again. It could be that insertSubview checks if the refresh already has a parent and removes it first, so might not be your issue. You should only do the insert when the controller appears for the first time.
updateData could also be getting added multiple times.
So I think you only need to assign self.refreshControl and then add a handler for the refresh action as you do now using addTarget but this time do it on self.refreshControl.
You can also do all this from storyboard. In storyboard you select the UITableViewController and on the attribute inspector simply set the Refreshing attribute to enabled. This adds a UIRefreshControl into the table and you can see it on the view hierarchy. You can then simply CTRL drag as normal from the refresh control into the .h file and add an action for valueChange which will be fired when you pull down on the refresh control in the table.
Well, I believe that your described behavior might not necessarily be caused by the refresh control.
According to the fact that you don't have any other subviews below your table view I would recommend you to try to place a "fake"-view below your table view. I usually prefer an empty label with 0 side length.
I had similar issues like yours where my table view insets were broken in some cases. And as soon as I used this "fake" subview the problems disappeared. I've read about this issue in some other threads, too. And the solution was this. Seems to be an odd behavior/bug.
Give it a try :)

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