Voice over speak tab bar below popup - ios

I have a modal(popup) in that when voice over reads the last element after that it start speaking tab bar elements.
I have used some property like:
view.accessibilityViewIsModal = true
UIAccessibility.post(notification: .layoutChanged, argument: self)
UIAccessibility.post(notification: .screenChanged, argument: self)
self.isAccessibilityElement = true
view.isAccessibilityElement = true
Expected : it should not read tab bar below popup

You can assign higher window to your view with some delay.
perform(#selector(self.assignWindowLevel), with: nil, afterDelay: 0.2)
#objc func assignWindowLevel() {
self.window?.windowLevel = UIWindow.Level.alert + 1.2
}

Related

Accessibility focus order not working as expected [iOS]

OVERVIEW
I'm having trouble getting correct focus order (Accessibility in iOS). It seems like becomeFirstResponder() overwrites my focus order I have specified in the array and causes Voice Over Accessibility functionality to read wrong Accessibility Label first.
DETAILS:
I have a View Controller with containerView. Inside I have UIView of my progress bar image and text input field (placeholder). Both elements have isAccessibilityElement = true attributes and they have been added to my focus order array. However upon screen launch, focus order goes to the input field instead of progress bar UIView.
After extended testing I've noticed that this issue is no longer replicable if I remove below line of code.
otpNumberTextField.becomeFirstResponder()
But this is not a solution. I need cursor in the textfield but Voice Over functionality to read my Progress Bar Accessibility Label first. How to fix it?
SPECIFIC SCENARIO
I've noticed this issue occurs only when I have VC with a last active focus on a Textfield and then transition to the next VC (with a Textfield and a Progress Bar).
Bug is not replicable when I have VC with a last active focus on the Button and then transition to the next VC (with a Textfield and a Progress Bar).
CODE SNIPPET
import UIKit
class MyViewController: UIViewController, UITextFieldDelegate {
var otpNumberTextField = UITextField()
var progressMainDot = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
setupView()
setupBinding()
}
override func viewWillAppear(_ animated: Bool) {
setupView()
textFieldDidChange(otpNumberTextField)
}
func setupView(){
let containerView = UIView()
containerView.backgroundColor = UIColor.init(named: ColourUtility.BackgroundColour)
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
//Progress Bar
let progressBarView = UIView()
containerView.addSubview(progressBarView)
progressBarView.isAccessibilityElement = true
progressBarView.accessibilityLabel = "my accessibility label"
progressBarView.translatesAutoresizingMaskIntoConstraints = false
progressMainDot.image = UIImage(named:ImageUtility.progressMain)
progressMainDot.contentMode = .scaleAspectFit
progressBarView.addSubview(progressMainDot)
//Text Field
otpNumberTextField.borderStyle = UITextField.BorderStyle.none
otpNumberTextField.font = UIFontMetrics.default.scaledFont(for: FontUtility.inputLargeTextFieldStyle)
otpNumberTextField.adjustsFontForContentSizeCategory = true
otpNumberTextField.isAccessibilityElement = true
otpNumberTextField.accessibilityLabel = AccessibilityUtility.enterVerificationCode
otpNumberTextField.placeholder = StringUtility.otpPlaceholder
otpNumberTextField.textColor = UIColor.init(named: ColourUtility.TextfieldColour)
otpNumberTextField.textAlignment = .center
otpNumberTextField.keyboardType = .numberPad
otpNumberTextField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
containerView.addSubview(otpNumberTextField)
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
}
//... more code goes here ...
}
If you have already set accessibilityElements, then voice over should respects that order but calling becomeFirstResponder() changes the focus to that text field.
You can try below code, which notifies voice over for shifting the focus to new element due to layout changes.
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
So now your modified method should be like below:
func setupView(){
.....
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
.....
}

Prevent Voice over announcement again before changing UIAccessibility focus to other element

I have a View . On double tap action I am hiding it .
But before posting to other view it announces once again .
Below is code snippet.
func setUpAccessibility() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(blockLabelViewSwipeGesture))
blockContainerView.accessibilityLabel = "Some text label"
blockContainerView.accessibilityHint = "Double tap to dismiss"
blockContainerView.isAccessibilityElement = true
blockContainerView.addGestureRecognizer(tapGesture)
}
#objc private func blockLabelViewSwipeGesture(_ gestureRecognizer: UISwipeGestureRecognizer {
UIAccessibility.post(notification: .layoutChanged, argument: self.headerView)
//Dismiss view
///DO some work
}
To solve this issue . In action method we need to set accessibilityLabel to empty and if accessibilityHint is there then set it to empty sting .
Below is code snippet .
#objc private func blockLabelViewSwipeGesture(_ gestureRecognizer: UISwipeGestureRecognizer {
blockContainerView.isAccessibilityElement = false
blockContainerView.accessibilityLabel = ""
blockContainerView.accessibilityHint = ""
UIAccessibility.post(notification: .layoutChanged, argument: self.headerView)
//Dismiss view
///DO some work
}

How to disable selection of tab bar items - swift

I have an application that has two UIViewcontroller embedded in a UITabBarcontroller. When I am in UIViewController-1, i would like to press a button that disables all item selection of the tab bar. My effort is below but I am not sure how to complete the code ...
When I am in the 'Folders' UIViewController I would like to disable the selection of any tab bar item:
class Folders: UIViewController, UITableViewDataSource, UITableViewDelegate{
...
// DISABLE TAB BAR ITEMS
func disable (){
let tabBarItemsArray = self.tabBarController?.tabBar.items
tabBarItemsArray[0].enabled = false // THIS BIT OF CODE IS NOT RECOGNIZED BY XCODE
}
...
}
tabBarItemsArray is optional, its type is [UITabBarItem]?.
You could initially force unwrap it: tabBarItemsArray![0], but the right way is to use if let construct:
if let tabBarItemsArray = tabBarController.tabBar.items {
tabBarItemsArray[0].isEnabled = false
}
or:
guard let tabBarItemsArray = tabBarController.tabBar.items else {
fatalError("Error")
}
let item = tabBarItemsArray[0]
item.isEnabled = false
You can do that using single line of code. Please check following code.
You can execute this from any controller.
self.navigationController?.tabBarController?.tabBar.items![0].isEnabled = false
Another way
You can define NotificationCenter observer to achieve. Please check following code. *In TabBar Controller file.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(disableTab(notification:)), name: Notification.Name("disableTab"), object: nil)
}
#objc func disableTab(notification: Notification) {
self.TabBarItem.isEnabled = false
}
Fire from anywhere as following...
NotificationCenter.default.post(name: Notification.Name("disableTab"), object: nil)
if you want to disable one tabbar item at once then this is for disabling the first one:
guard let tabbars = self.tabBar.items else {
return
}
tabbars[0].isEnabled = false
but if you want them all to be disabled at once then this is the one to be implemented:
self.tabBar.items?.map{$0.isEnabled = false}

iOS Dark Keyboard has background during UINavigationController push/pop animation

During the navigation controller push/pop animation, keyboard is darker then it is in its final state. And on animation end, this black background view just disappears. Light (white) keyboard style does not have this effect.
How can I get rid of this black background?
I already tried setting the window color to white and setting the navigation controller background to white.
VIDEO:
https://www.dropbox.com/s/z1grj821fj306th/Untitled.mov?dl=0
SCREENSHOT:
Option 1
The easiest solution is to just to disable the keyboard transparency by setting its background color to black or white (depending on if you have a light or dark keyboard):
myTextField.becomeFirstResponder()
guard UIDevice().userInterfaceIdiom == .phone else {
return;
}
//keyboard window should be there now, look for it:
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.subviews.first?.backgroundColor = .black
Option 2
If you don't like this I have a hack that seems to work pretty well, at least in iOS 14. It results in the keyboard slide up animation occurring during the slide in push animation, instead of after. It relies on showing the keyboard right before pushing by adding a temporary text field.
Run this code whenever you want to push the VC:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then right after push VC, then discard the text field
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
tempTextField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
}
Option 3
I improved on option 2 by copying the push slide animation to the keyboard, essentially resulting in exactly what you asked. There is also the option to keep the sliding up animation too during the slide in, just give it a try.
Run this code on the pushing VC whenever you want to push:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then push VC, then remove the text field (see bottom)
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
UIView.setAnimationsEnabled(false) //set to false to disable slide up animation or true to keep it
tempTextField.becomeFirstResponder()
UIView.setAnimationsEnabled(true)
//find keyboard window
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.isHidden = true //this prevents glitches
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
keyboardWindow?.rootViewController?.view.isHidden = false
//this spring animation is identical to default push slide animation
let spring = CASpringAnimation(keyPath: "position")
spring.damping = 500
spring.mass = 3
spring.initialVelocity = 0
spring.stiffness = 1000
spring.fromValue = CGPoint.init(x: self.view.frame.width, y:0) //you can enter e.g y:1000 to delay slide up animation
spring.toValue = CGPoint.init(x: 0, y:0)
spring.duration = 0.5
spring.isAdditive = true
keyboardWindow?.layer.add(spring, forKey: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
You can keep the sliding up animation by not disabling the animations (above becomeFirstResponder()). If you choose this it is possible to delay the slide up animation by replacing y:0 with y:1000 for example. You can play with this value.

iOS change accessibility focus

Is there a way to set the accessibility focus programatically (App Store safe)? Any help will be greatly appreciated.
To focus on element you can call.
Swift:
UIAccessibility.post(notification: .screenChanged, argument: self.myFirstView)
ObjC:
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.myFirstView);
Otherwise for iOS 8+ use the accessibilityElements to set element order and it will focus automatically on first element in the list
self.accessibilityElements = #[myFirstView, mySecondButton, myThirdLabel]
extension UIAccessibility {
static func setFocusTo(_ object: Any?) {
if UIAccessibility.isVoiceOverRunning {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
UIAccessibility.post(notification: .layoutChanged, argument: object)
}
}
}
}
Add this extension and call it by passing in the view you would like to be focused. If you would like to change focus when navigating using a tabbar, you can call this from viewWillAppear. This code wont work in any init method without a the delay of 0.7 or more.
This is the Swift code:
UIAccessibility.post(notification: .screenChanged, argument: <theView>)
Example usage
let titleLabel = UILabel()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(notification: .screenChanged, argument: titleLabel)
}
UIAccessibility.post(notification: .layoutChanged, argument: toast)
toast.becomeFirstResponder()
toast.isAccessibilityElement = true
toast.accessibilityTraits = .staticText //Traits option ie .header, .button etc
toast.accessibilityLabel = "Accessibility label to be read by VoiceOver goes here"
WHERE:
toast is my UIView (a pop up which gets triggered upon particular scenario)
above code needs to be added to the toast View
this code will automatically change focus to pop up view (toast) and VoiceOver will read accessibilityLabel

Resources