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)
.....
}
Related
I am using MessageKit. I've created a MessagesViewController. I add messageInputBar as a subview from viewDidLoad along with a navigational bar that includes a back button. Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController. If I don't tap on the messageInputBar when i first enter the MessagesViewController and press the back button, the messageInputBar properly is dismissed. Below is my code
override func viewDidLoad() {
super.viewDidLoad()
setUpNavBar()
navigationItem.largeTitleDisplayMode = .never
maintainPositionOnKeyboardFrameChanged = true
scrollsToLastItemOnKeyboardBeginsEditing = true
messageInputBar.inputTextView.tintColor = .systemBlue
messageInputBar.sendButton.setTitleColor(.systemTeal, for: .normal)
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
loadChat()
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
}
func setUpNavBar() {
let navBar = UINavigationBar()
self.view.addSubview(navBar)
navBar.items?.append(UINavigationItem(title: (selectedUser?.userFirstName)!))
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonTapped))
navBar.topItem?.leftBarButtonItem = backButton
navBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
navBar.heightAnchor.constraint(equalToConstant: 44),
navBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
#IBAction func backButtonTapped(_ sender: Any) {
let transition = CATransition()
self.view.window!.layer.add(transition.segueLeftToRight(), forKey: kCATransition)
self.dismiss(animated: false)
}
As you have created MessagesViewController, you don't need to explicitly add messageInputBar to the bottom of the view.
Let's look at the source of MessageKit
private func setupInputBar(for kind: MessageInputBarKind) {
inputContainerView.subviews.forEach { $0.removeFromSuperview() }
func pinViewToInputContainer(_ view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
inputContainerView.addSubviews(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
])
}
switch kind {
case .messageInputBar:
pinViewToInputContainer(messageInputBar)
case .custom(let view):
pinViewToInputContainer(view)
}
}
The following code section should be removed from your source as the messageInputBar has already been set up in the library.
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
Now, your scenario
Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController.
Whenever, there is an object that you interact with (eg. messageInputBar), and it is not deallocated (stays in view) after you dismissed the view controller, there is a memory leak.
If you repeatedly enter and dismiss the view controller, you will observe a rise in the memory usage of the app. So, finding out which object is creating this retain cycle, should solve this issue.
I installed MessageKit using Cocoapods later to find out they dropped support for Cocoapods. So I completely migrated my entire project over to Swift Package Manager to get the latest MessageKit which includes the setup for the inputbar in their code. No idea why they would release a version that didn't have this initially? Anyways, solved my problem!
Goal:
Hide a UIButton(w/ image) once a view is hidden.
I have a layout whereby the map view can be hidden when user taps(UITapGestureRecognizer) on the screen. When this happen, I would like to hide the "follow user button" triangle. Currently I am not able to do it.
What I've tried: (from multiple google/SO posts)
1)
followUserButton.removeFromSuperview()
followUserButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.setImage(image:nil for: .normal)
the last one I tried is basically just making the image black in color (to blend into the background). This does look like a success, but (see gif image), for some reason, the first click, will still show the button (very light black/grey - in the bottom middle of image). Click again, the map view comes on, then click again and it finally disappears
followUserButton.tintColor = .black
followUserButton.isHidden = true
this is how I'm adding my button programatically
var followUserImage: UIImage!
var followUserButton: UIButton!
override func viewDidLoad() {
setupFollowUserButton()
}
func setupFollowUserButton() {
addFollowUserButton()
self.view.addSubview(followUserButton)
constraintFollowUserButton()
}
func hideFollowUserButton() {
if vcTrainMapView.isHidden {
if followUserButton != nil {
// followUserButton.removeFromSuperview()
// followUserButton.tintColor = .black
followUserButton.isHidden = true
}
} else if followUserButton != nil {
followUserButton.tintColor = .lightGray
}
}
func addFollowUserButton() {
followUserButton = UIButton(type: UIButton.ButtonType.custom)
followUserImage = UIImage(named: "follow_user_high")
followUserButton.setImage(followUserImage, for:.selected)
followUserImage = (UIImage(named: "follow_user"))
followUserButton.setImage(followUserImage, for: .normal)
followUserButton.tintColor = .lightGray
followUserButton.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
}
#objc private func buttonAction(_ sender: UIButton) {
self.followUserStatus = !self.followUserStatus
sender.isSelected.toggle()
}
func constraintFollowUserButton() {
followUserButton.translatesAutoresizingMaskIntoConstraints = false
followUserButton.bottomAnchor.constraint(equalTo: vcTrainMapView.bottomAnchor, constant: -10).isActive = true
followUserButton.leadingAnchor.constraint(equalTo: vcTrainMapView.leadingAnchor, constant: 10).isActive = true
followUserButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
this is what I've achieved w/ #3 above. (the button is very light in the gif)
If you're going to be hiding and showing a button at the same time as a detail view (your map view), and you're displaying the button so that it looks like it's on that view, you could just add the button directly to that view rather than the view controller main view.
You can, of course, still control the action of the button from the view controller, but if it's added to the map view then the button will be hidden when the map view is hidden.
In my app, I want a three-column UISplitViewController. I create it like this:
let svc = UISplitViewController(style: .tripleColumn)
svc.preferredDisplayMode = .twoOverSecondary
svc.setViewController(TestViewController(), for: .primary)
svc.setViewController(TestViewController(), for: .supplementary)
svc.setViewController(TestViewController(), for: .secondary)
svc.primaryBackgroundStyle = .sidebar
This ViewController that I'm presenting is a really simple viewController. It doesn't do anything except, present a centered red square.
class TestViewController: UIViewController {
private var redView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(redView)
redView.widthAnchor.constraint(equalToConstant: 30).isActive = true
redView.heightAnchor.constraint(equalToConstant: 30).isActive = true
redView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
}
Now, when I run this code, I get the following result.
As you can see, 2 of the 3 squares are off-center. Anyone know, what I'm doing wrong here? Or is this a known bug?
You should use "Safe Area" instead of "self.view" when setting to center.
The detail view here has a push-away effect. You just need to click on it and the primary view controller will hide, like below.
Note: I have attempted using .becomeFirstResponder() as shown in the link above. I stupidly left that detail out in my first post. See my code for edits.
I'm using a UISearchBar in it's own dedicated viewController (the code used for the search bar is from Google as part of their SDK). As soon as the app segues into that viewController, I want the keyboard to appear immediately without user intervention, and I don't want it to disappear. More importantly, I would like the cancel button to remain visible at all times.
I'm already aware of how to make the keyboard disappear using resignFirstResponder. I tried using .becomeFirstResponder() to no effect. However, looking on StackOverflow, out on Google, and in Apple's documentation, I'm not seeing a way to make it appear without user intervention.
All of the functions, such as editingDidBegin() require the user to do something.
This feels pretty basic, but I'm coming up empty.
private var resultsViewController: GMSAutocompleteResultsViewController?
private var searchController: UISearchController?
private var resultView: UITextView?
// tableview code for Google autocomplete
private var tableDataSource: GMSAutocompleteTableDataSource?
// not including irrelevant code...
// called in viewDidLoad()
func displaySearchBar(){
let searchVerticalLocation = UIScreen.main.bounds.height-UIScreen.main.bounds.height+33
resultsViewController = GMSAutocompleteResultsViewController()
resultsViewController?.delegate = self
//resultsViewController.becomeFirstResponder() doesn't work
searchController = UISearchController(searchResultsController: resultsViewController)
searchController?.searchResultsUpdater = resultsViewController
let subView = UIView(frame: CGRect(x: 0, y: searchVerticalLocation, width: 350, height: 60))
searchController?.searchBar.barTintColor = UIColor(red: 234/255.0, green: 93/255.0, blue: 0/255.0, alpha: 1.0)
searchController?.searchBar.keyboardAppearance = .dark
searchController?.searchBar.searchBarStyle = .prominent
searchController?.searchBar.placeholder = "enter destination"
searchController?.searchBar.isTranslucent = true
searchController?.searchBar.leftAnchor.constraint(equalTo: subView.leftAnchor, constant: 100)
searchController?.searchBar.rightAnchor.constraint(equalTo: subView.rightAnchor, constant: -100)
searchController?.searchBar.delegate = self
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes([NSAttributedString.Key(rawValue: NSAttributedString.Key.foregroundColor.rawValue): UIColor.white], for: .normal)
//Everything with this tap recognizer is an attempt to ensure that the cancel button on the searchbar doesn't disappear.
let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.cancelSearchBar(sender:)))
singleTapGestureRecognizer.delegate = self
singleTapGestureRecognizer.numberOfTapsRequired = 1
singleTapGestureRecognizer.isEnabled = true
singleTapGestureRecognizer.cancelsTouchesInView = false
searchController?.searchBar.addGestureRecognizer(singleTapGestureRecognizer)
subView.addSubview((searchController?.searchBar)!)
view.insertSubview(subView, at: 1)
// When UISearchController presents the results view, present it in
// this view controller, not one further up the chain.
definesPresentationContext = true
searchController?.isActive = true
searchController?.searchBar.becomeFirstResponder()
//searchController?.becomeFirstResponder() doesn't work either
}
// this code brings back the cancel button if the user taps in the searchbar's text field. It's an imperfect solution. I'd rather have it so that the searchbar doesn't disappear at all. Not sure how to make that happen yet.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
searchController?.isActive = true
return true
}
The keyboard and cancel button currently only appear when the user taps inside the text field. Tapping anywhere else on the screen causes them to disappear.
For the cancel button, you can make it appear automatically using searchBar.showsCancelButton = true. However, this will not respond to touches until the keyboard appears.
Calling .becomeFirstResponder on the searchBar will only work after the searchController is finished loading. Please reference the following article: Cannot set searchBar as firstResponder.
Calling searchBar.becomeFirstResponder() in viewDidAppear(), and even forcing it onto the main thread using DispatchQueue.main.async{} (as some people suggest) did not work. The following code snippet, called from viewDidLoad(), is my current working solution:
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(700), execute: {
self.searchController?.searchBar.becomeFirstResponder()
})
I may adjust the delay to make it longer, to ensure that it works on all devices. This works on an iPhoneXR.
I'm trying to get/keep a handle on elements in my UIStackView after I have moved it with a pan gesture.
For example, the elements I am trying to grab a handle on are things like the button tag or the text label text.
The code explained…
I am creating a UIStackView, via the function func createButtonStack(label: String, btnTag: Int) -> UIStackView
It contains a button and a text label.
When the button stack is created, I attach a pan gesture to it so I can move the button around the screen. The following 3 points work.
I get the button stack created
I can press the button and call the fun to print my message.
I can move the button stack.
The issue I have is…
Once I move the button stack the first time and the if statement gesture.type == .ended line is triggered, I lose control of the button stack.
That is, the button presses no longer work nor can I move it around any longer.
Can anyone please help? Thanks
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let ButtonStack = createButtonStack(label: “Button One”, btnTag: 1)
view.addSubview(ButtonStack)
ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
ButtonStack.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let panGuesture = UIPanGestureRecognizer(target: self, action: #selector(pan(guesture:)))
ButtonStack.isUserInteractionEnabled = true
ButtonStack.addGestureRecognizer(panGuesture)
}
func createButtonStack(label: String, btnTag: Int) -> UIStackView {
let button = UIButton()
button.setImage( imageLiteral(resourceName: "star-in-circle"), for: .normal)
button.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
button.contentMode = .scaleAspectFit
button.tag = btnTag
switch btnTag {
case 1:
button.addTarget(self, action: #selector(printMessage), for: .touchUpInside)
case 2:
break
default:
break
}
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.green
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
textLabel.font = textLabel.font.withSize(15)
textLabel.text = label
textLabel.textAlignment = .center
//Stack View
let buttonStack = UIStackView()
buttonStack.axis = UILayoutConstraintAxis.vertical
buttonStack.distribution = UIStackViewDistribution.equalSpacing
buttonStack.alignment = UIStackViewAlignment.center
buttonStack.spacing = 1.0
buttonStack.addArrangedSubview(button)
buttonStack.addArrangedSubview(textLabel)
buttonStack.translatesAutoresizingMaskIntoConstraints = false
return buttonStack
}
#objc func printMessage() {
print(“Button One was pressed”)
}
#objc func pan(guesture: UIPanGestureRecognizer) {
let translation = guesture.translation(in: self.view)
if let guestureView = guesture.view {
guestureView.center = CGPoint(x: guestureView.center.x + translation.x, y: guestureView.center.y + translation.y)
if guesture.state == .ended {
print("Guesture Center - Ended = \(guestureView.center)")
}
}
guesture.setTranslation(CGPoint.zero, in: self.view)
}
If you're using autolayout on the buttonStack you can't manipulate the guestureView.center centerX directly. You have to work with the constraints to achieve the drag effect.
So instead of ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true you should do something along the lines of:
let centerXConstraint = ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
centerXConstraint.isActive = true
ButtonStack.centerXConstraint = centerXConstraint
To do it like this you should declare a weak property of type NSLayoutConstraint on the ButtonStack class. You can do the same thing for the centerY constraint.
After that in the func pan(guesture: UIPanGestureRecognizer) method you can manipulate the centerXConstraint and centerYConstraint properties directly on the ButtonStack view.
Also, I see you are not setting the translatesAutoresizingMaskIntoConstraints property to false on the ButtonStack. You should do that whenever you are using autolayout programatically.
Thanks to PGDev and marosoaie for their input. Both provided insight for me to figure this one out.
My code worked with just the one button, but my project had three buttons inside a UIStackView.
Once I moved one button, it effectively broke the UIStackView and I lost control over the moved button.
The fix here was to take the three buttons out of the UIStackView and I can now move and control all three buttons without issues.
As for keeping a handle on the button / text field UIStackView, this was achieved by adding a .tag to the UIStackView.
Once I moved the element, the .ended action of the pan could access the .tag and therefore allow me to identify which button stack was moved.
Thanks again for all of the input.