iOS change accessibility focus - ios

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

Related

Voice over speak tab bar below popup

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
}

How to setup SearchController/SearchBar/SearchResultsController in Swift

So in the screen recording, I want to create a search controller/search-bar like instacart has. I created a collection-view that has a search-bar in the navigation bar (via search controller). I want to show suggested searches (like instacarts' "Popular searches") when the search-bar is selected (instead of dimming the background and showing the collection-view - like its default function). And then once the user begins typing I want to have a table view that gets filtered and the filtered data gets presented (replacing the suggested searches) - like shown in the pictures. Ultimately I'm having trouble presenting suggested searches once the search-bar is clicked and presenting a filtered table once user begins typing in the search bar. Basically I would like to be able to do what instacart accomplished here. Any help would be greatly appreciated. Thanks!
Reference pictures
when you click the search bar from main category just navigate to the search suggestions.
In my case I used custom searchbar from UITextField.
I created 2 viewControllers 1 is for suggested results and other one is for showing search results.
in the searchsuggestionsVC viewDidload i am checking the character count and calling and loading suggested products in a table view as below
self.searchTF.addTarget(self, action: #selector(searchRecords(_ :)), for: .editingChanged)
create the searchRecords selecter as below
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if self.searchTF.text != "" {
//any task to perform
//if you want to dismiss your keyboard
print("Pro search keyboard button tapped")
self.proSearchQuery = self.searchTF.text ?? ""
UserDefaults.standard.set(self.proSearchQuery, forKey: "kProSearchQuery")
print("PRO SEARCH QUERY : \(self.proSearchQuery ?? "")")
self.navigatTo(identifier: "SearchResultsVC", animated: false)
}
else{
self.showCustomToast(message: "Please type something to search")
}
return true
}
And Used following delegates to navigate to the search results by tapping search button in the keyboard.
override func viewDidAppear(_ animated: Bool) {
self.searchTF.becomeFirstResponder()
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
print("text cleared")
return true
}
// search button execution
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if self.searchTF.text != "" {
//any task to perform
//if you want to dismiss your keyboard
print("Pro search keyboard button tapped")
self.proSearchQuery = self.searchTF.text ?? ""
UserDefaults.standard.set(self.proSearchQuery, forKey: "kProSearchQuery")
print("PRO SEARCH QUERY : \(self.proSearchQuery ?? "")")
self.navigatTo(identifier: "SearchResultsVC", animated: false)
}
else{
self.view.makeToast("Please type something to search")
}
return true
}
May be above code is not as per your requirements but hope you will get some Ideas on it. let me know in comments if anything not clear or add up.

How to Focus Accessibility On A Particular Segment in A UISegmentedControl

OK. This answer helps a lot. I can select an accessibility item when a screen is shown. I simply add
UIAccessibility.post(notification: .layoutChanged, argument: <a reference to the UI item to receive focus>)
to the end of my viewWillAppear() method, and the item receives focus.
However, in one of my screens, the item I want to receive focus is a UISegmentedControl, and, when focused, it always selects the first item, no matter which one is selected. Since I followed the excellent suggestion here, I have an accessibility label for each item in the control, and I'd like my focus to begin on whichever segment is selected.
Is there a way to do this? As a rule, I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Thanks!
UPDATE: Just to add insult to injury, I am also having an issue with the item I want selected being selected, then a second later, the screen jumps the selection to the first item. That's probably a topic for a second question.
I created a blank project as follows to reproduce the problem:
The solution is taking the selectedIndex to display the selected segment and providing the appropriate segment object for the VoiceOver notification: easy, isn't it?
I naively thought that getting the subview in the segmented control subviews array with the selectedIndex would do the job but that's definitely not possible because the subviews can move inside this array as the following snapshot highlights (red framed first element for instance):
The only way to identify a unique segment is its frame, so I pick up the segmented control index and the frame of the selected segment to pass them to the previous view controller.
That will allow to display (index) and read out (frame that identifies the object for the notification) the appropriate selected segment when this screen will appear after the transition.
Hereafter the code snippets for the view controller that contains the 'Next Screen' button:
class SOFSegmentedControl: UIViewController, UpdateSegmentedIndexDelegate {
var segmentIndex = 0
var segmentFrame = CGRect.zero
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let segueName = segue.identifier {
if (segueName == "SegmentSegue") {
if let destVC = segue.destination as? SOFSegmentedControlBis {
destVC.delegate = self
destVC.segmentIndex = segmentIndex
destVC.segmentFrame = segmentFrame
}
}
}
}
#IBAction func buttonAction(_ sender: UIButton) { self.performSegue(withIdentifier: "SegmentSegue", sender: sender) }
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect) {
segmentIndex = index
segmentFrame = frame
}
}
... and for the view controller that displays the segmented control:
protocol UpdateSegmentedIndexDelegate: class {
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect)
}
class SOFSegmentedControlBis: UIViewController {
#IBOutlet weak var mySegmentedControl: UISegmentedControl!
var delegate: UpdateSegmentedIndexDelegate?
var segmentFrame = CGRect.zero
var segmentIndex = 0
var segmentFrames = [Int:CGRect]()
override func viewDidLoad() {
super.viewDidLoad()
mySegmentedControl.addTarget(self,
action: #selector(segmentedControlValueChanged(_:)),
for: .valueChanged)
mySegmentedControl.selectedSegmentIndex = segmentIndex
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(mySegmentedControl.subviews)
let sortedFrames = mySegmentedControl.subviews.sorted(by: { $0.frame.origin.x < $1.frame.origin.x})
for (index, segment) in sortedFrames.enumerated() { segmentFrames[index] = segment.frame }
if (self.segmentFrame == CGRect.zero) {
UIAccessibility.post(notification: .screenChanged,
argument: mySegmentedControl)
} else {
mySegmentedControl.subviews.forEach({
if ($0.frame == self.segmentFrame) {
UIAccessibility.post(notification: .screenChanged,
argument: $0)
}
})
}
}
#objc func segmentedControlValueChanged(_ notif: NSNotification) {
delegate?.updateSegmentIndex(mySegmentedControl.selectedSegmentIndex,
withFrame: segmentFrames[mySegmentedControl.selectedSegmentIndex]!) }
}
The final result is as follows:
Double tap to go to the next screen.
Select the next element to focus the second segment.
Double tap to select the focused element.
Get back to the previous screen thanks to the Z gesture natively known by iOS with the navigation controller. The delegate passes the index and the frame of the selected segment.
Double tap to go to the next screen.
The segment that was formerly selected is read out by VoiceOver and still selected.
You can now Focus Accessibility On A Particular Segment in A UISegmentedControl following this rationale.
I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Unfortunately, this solution is a hacky one... sorry. However, it works and I couldn't find another one anywhere else: see it as a personal fix unless you get a cleaner one to share? ;o)
UPDATE... That's probably a topic for a second question.
I can't reproduce the behavior of your update: if you create a dedicated topic for this problem, please add the most detailed code and context so as to provide the most accurate solution.
i think this works~!
class VC {
let segment = UISegmentedControl()
func fucusSegment(index: Int) {
let item = segment.accessibilityElement(at: index )
UIAccessibility.post(notification: .layoutChanged, argument: item)
}
}

Move keyboard above TabViewController TabBar

Is it possible to move the keyboard up so it doesn't cover the UITabViewController's TabBar?
Update after being given more context in comments
If your main concern is letting the user dismiss the keyboard, there are some well known patterns that are commonly applied on the platform:
Assumption regarding UI (derived from your comment):
- UITableView as main content
To make the keyboard dismissible, you can utilise a property on UIScrollView called .keyboardDismissMode. (UITableView is derived from UIScrollView, so it inherits the property.)
The default value for this property is .none. Change that to either .onDrag or .interactive. Consult the documentation for differences between the latter two options.
Behind the scenes, UIKit sets up a connection between the UIScrollView instance and any incoming keyboard. This allows the user to "swipe away" the keyboard by interacting with the scroll view.
Note that in order for this feature to work, your UIScrollView needs to be scrollable. To understand what 'scrollable' means in this context, please see this gist.
If your tableView has very few or no rows, it is likely not natively scrollable. To account for that, set tableView.alwaysBounceVertical = true. This will make sure your users can dismiss the keyboard regardless of the number of rows in the table.
Most of the popular apps handling keyboard dismissal also make it possible to dismiss the keyboard simply by tapping the content partially overlapped by it (in your case, the tableView). To enable this, you would simply have to install a UITapGestureRecognizer on your view and dismiss the keyboard in its action method:
class MyViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapRecognizer)
}
}
//MARK: - Tap handling
fileprivate extension MyViewController {
#objc func handleTap() {
if searchBar.isFirstResponder {
searchBar.resignFirstResponder()
}
// Alternative
// view.endEditing(true)
}
}
// -
Old answer
Yes, you can actually do this without using private API.
Disclaimer
You should really think about whether you actually want to do this. Opening the keyboard in virtually every use case should create a new "context" of editing which modally "blocks" other contexts (such as the navigation context provided by UITabBarController and its UITabBar). I guess one could make the point that users are able to leave an editing context by interacting with a potentially present UINavigationBar which is usually not blocked by keyboards. However, this is a known interaction throughout the system. Not blocking a UITabBar or UIToolbar while showing the keyboard on the other hand, is not. That being said, use the code below to move the keyboard up, but critically review the UX you are creating. I'm not to say it does never make sense to move the keyboard up, but you should really know what you're doing here. To be honest, it also looks kind of iffy, having the keyboard float above the tab bar.
Code
extension Sequence {
func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
return try reversed().first(where: predicate)
}
}
// Using `UIViewController` as an example. You could and actually should factor this logic out.
class MyViewController: UIViewController {
deinit {
NotificationCenter.default.removeObserver(self)
}
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: .UIKeyboardWillHide, object: nil)
}
}
//MARK: - Keyboard handling
extension MyViewController {
private var keyboardOffset: CGFloat {
// Using a fixed value of `49` here, since that's what `UITabBar`s height usually is.
// You should probably use something like `-tabBarController?.tabBar.frame.height`.
return -49
}
private var keyboardWindowPredicate: (UIWindow) -> Bool {
return { $0.windowLevel > UIWindowLevelNormal }
}
private var keyboardWindow: UIWindow? {
return UIApplication.shared.windows.last(where: keyboardWindowPredicate)
}
#objc fileprivate func keyboardWillShow(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = keyboardOffset
}
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = 0
}
}
}
// -
Caution
Note that if you are using the .UIKeyboardWillShow and .UIKeyboardWillHide notifications to account for the keyboard in your view (setting UIScrollView insets, for example), you would have to also account for any additional offset by which you move keyboard window.
This works and is tested with iOS 11. However, there is no guarantee that the UIKit team won't change the order of windows or something else that breaks this in future releases. Again, you are not using any private API, so AppStore review should not be in danger, but you are doing something that you're not really supposed to do with the framework, and that can always come around and bite you later on.

How to dismiss keyboard with multiple UITextField

I'm a noob here and in iOS world. I am having trouble dismiss keyboard on a specific case in my very simple todo list iOS app.
I'd like the keyboard to get dismiss when user taps anywhere outside the current text field or the keyboard itself. So far, I got the keyboard dismisses just fine (thanks to you guys here in stack overflow) when user taps on the UITableView, or most element on my app. HOWEVER, when user taps on another UITextField, the keyboard does not go away.
FYI, here's the list of existing threads I researched so far but have yet to solve this issue.
1) How to dismiss keyboard iOS programmatically
2) Resigning First Responder for multiple UITextFields
3) Dismissing the First Responder/Keyboard with multiple Textfields
4) (a few more at least but I lost track :( )
Here's what I did so far:
(in viewDidLoad())
// Add 'tap' gesture to dismiss keyboard when done adding/editing to-do item
var tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tapOutside:")
tap.cancelsTouchesInView = true
self.view.addGestureRecognizer(tap)
func tapOutside(tapOutside: UIGestureRecognizer) {
// Dismiss keyboard
self.view.endEditing(true)
}
#IBAction func EditingDidBegin(sender: UITextField) {
// Highlight the text field which user is editing
self.highlightTextField(sender, highlight: true)
}
#IBAction func EditingDidEnd(sender: UITextField) {
// Undo text field highlight
self.highlightTextField(sender, highlight: false)
self.view.endEditing(true) // try this option and not working
self.setEditing(false, animated: true) // try this option and not working
sender.resignFirstResponder() // try this option and not working
UIApplication.sharedApplication().becomeFirstResponder() // try this option and not working
... // below is my code to update the todo item
}
I also tried to print out all subviews.isFirstResponder() of my view. All of it return false. I also tried override touchesBegan of my UIViewController, and inside it just calls self.view.endEditing(true) and call its super's. This also does not work.
Please help. :(
TIA!
UPDATE:
You guys are awesome! :D I got it working now thanks to you guys. There were several mistakes / messed up as I'm learning new framework. So here's what I did.
1) I did not set UITextField delegate correctly.
Mistake: I ctrl-draged textfield in xcode and link my viewController as delegate and thought that should work out. I will still need to research and understand better why.
Solution: I removed that ctrl-drag link and explicitly call myTextField.delegate = self in tableView:cellForRowAtIndexPath. And that did it. Thanks #Sidewalker
2) Mistake: I have a mixed of textFieldShouldBeginEditing, etc. and #IBAction func EditingDidBegin. So I got myself into the situation where textFieldShouldBeginEditing got the call, but EditingDidBegin did not get call.
Solution: Once I set the delegate = self explicitly and stick with implementing textField... methods and not use any #IBAction for textField, things just work.
Here's one option... We're going to add a boolean flag to determine whether or not we're in a textField when an edit attempt for another textField begins
Make your class adhere to UITextFieldDelegate
class MyClass: UIViewController, UITextFieldDelegate
Don't forget to set the delegate, we'll add the flag as well
myTextField.delegate = self
var inField = false
Implement "textFieldShouldBeginEditing" and "textFieldDidBeginEditing"
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
if inField {
inField = false
return false
}
return true
}
func textFieldDidBeginEditing(textField: UITextField) {
inField = true
}
I prefer tracking things like this rather than identifying subviews as it allows the flag to be utilized elsewhere and cuts down code complexity.
Well the keyboard isn't going away because it doesn't expect to have to. The new UITextField is just becoming the first responder while the other resigns. If you don't want a textField to become the first responder if another is already, you're going to have to cut it off before it gets the chance to. I would try to implement textFieldShouldBeginEditing and figuring out the logic there.
I'm not in love with the way this looks but this should do something along those lines.
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
for subView in self.view.subviews{
if(subView.isKindOfClass(UITextField)){
if(subView.isFirstResponder()){
subView.resignFirstResponder();
return false;
}
}
}
return true;
}
First set all the UITextField (your are creating) delegate as self and create one UITextField member variable. Now implement "textFieldDidBeginEditing" delegate method and assign the textfield to your member UITextField variable. As given below
func textFieldDidBeginEditing(textField: UITextField) {
yourMemberVariable = textField;
}
So now whenever you want to dismiss the keyboard call the dismiss method on "yourMemberVariable" object. It should work !!
What I usually do is implementing this two method:
The first one add a UITapGestureRecognizer to the whole UIViewController view
func hideKeyboard() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tap)
}
The second one just get called every time the user touch anywhere on the UIViewController's view
func dismissKeyboard() {
self.view.resignFirstResponder()
}
I add the first one to the viewDidLoad method of the UIViewController. Or better yet if you want to use that on all the app just make that an extension for your UIViewController.
How about doing this in viewController, It works for me
func dismissKeyboard() {
//All the textFields in the form
let textFields = [textField1, textField2, textField3, textField4, textField5]
let firstResponder = textFields.first(where: {$0.isFirstResponder ?? false })
firstResponder?.resignFirstResponder()
}

Resources