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.
Related
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)
.....
}
I am working on a sync feature for my app.
What I want to achieve is to display a custom UIView that will serve as an indicator to the user, wherever screen the user at (such as navigate to dashboard, settings, profile page, etc), that the synchronization is in progress. In short term, it will stick to a position within the app statically.
After done some researches from the web, I come to a conclusion to use keyWindow and add a subview to it.
Here is my code
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let window = UIApplication.shared.keyWindow {
window.windowLevel = .statusBar
let uiView = UIView()
uiView.backgroundColor = .green
window.addSubview(uiView)
uiView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiView.heightAnchor.constraint(equalToConstant: 150),
uiView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25),
uiView.topAnchor.constraint(equalTo: view.topAnchor, constant: 150),
uiView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4)
])
}
}
Above code is working fine, I can attach a custom view to the keyWindow. However, when the user navigate to another screen, the added customView will dissappear, and only show when the user goes back to the previous screen.
Can someone guide on what part I am missing? On which part I did wrong?
Thanks
As per comments I am adding a minimum requirement to create a new window which will can now always be on top of your view controller or even other windows:
class OverlayViewController: UIViewController {
private var myWindow: UIWindow? // Our own window needs to be retained
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
view.addSubview({
let label = UILabel(frame: .zero)
label.text = "This is an overlay."
label.sizeToFit()
label.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
return label
}())
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap() {
myWindow?.isHidden = true
myWindow?.removeFromSuperview()
myWindow = nil
}
static func showInNewWindow() {
let window = UIWindow(frame: UIScreen.main.bounds)
// window.windowLevel = .alert
// window.isUserInteractionEnabled = false
window.rootViewController = {
let controller = OverlayViewController() // OR UIStoryboard(name: "<#FileName#>", bundle: nil).instantiateViewController(withIdentifier: "<#Storyboard Identifier#>")
controller.myWindow = window
return controller
}()
window.makeKeyAndVisible()
}
}
So this is just all-in-code simple view controller example which can be used anywhere in the project by simply calling OverlayViewController.showInNewWindow(). The view controller can be from storyboard though.
The interesting part is the showInNewWindow which creates a new window and an instance of this view controller that is set as a root view controller. To show the new window all you need to call is makeKeyAndVisible. And to remove it simply hid it and remove it from superview as done in onTap. In this case the window is referenced by the view controller which should create a retain cycle. It seems that this is mandatory. It also means that we need to do some cleanup once window is dismissed by calling myWindow = nil.
There are some settings possible as well like you can disable user interaction and enable user to still use the window below this one (window.isUserInteractionEnabled = false). And you can set the level of window as to where is it displayed (window.windowLevel = .alert). You can play around a bit with these values. Which to set depends on what level you want your window to be.
I have a viewController EditMessage which has two UITextFields (UITextView) which use the keyboard and they work great. This part is basic standard stuff. When the keyboard is displayed, I register a tag gesture for the entire view, so that if the user clicks anywhere else, I dismiss the keyboard:
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self,
action: #selector(dismissKeyboard)))
In dismissKeyboard, this all works fine:
#objc func dismissKeyboard(sender: Any) {
self.view.endEditing(true)
}
However, I have a menu button(thumbnail image) implemented as a child view controller (UIViewController) on the same EditMessage view, which hijacks the screen via UIApplication.shared.keyWindow() to display an overlay and menu on the bottom of the screen. Built using the model/code from Brian Voong's YouTube channel to replicate a YouTube style slide in menu from the bottom. However, the keyboard is in the way. Since the child is a different view controller "endEditing" doesn't work (or maybe I am referencing the wrong view?).
class ButtonPickerController : UIViewController,
UIGestureRecognizerDelegate, UINavigationControllerDelegate {
var maxSize = CGFloat(60)
let thumbnail: UIImageView = {
let thumbnail = UIImageView()
thumbnail.backgroundColor = UIColor.lightGray
return thumbnail
}()
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(self.buttonTapped(sender:)))
tap.delegate = self
view.addGestureRecognizer(tap)
//view.backgroundColor = .yellow
view.contentMode = .scaleAspectFit
thumbnail.frame = CGRect(x: 0, y: 0, width: self.maxSize, height: self.maxSize)
setupSubviews()
}
Can someone point me in a good direction? This is my first question so hopefully I am asking properly.
I figured it out in the end. Thank you for the help. In my child view controller I did used the following statement when the button was tapped:
#objc func buttonTapped(sender: UITapGestureRecognizer? = nil) {
self.parent!.view.endEditing(true)
}
First, its not a good way to present overlays as UIViewController.
But a solution good be, to give the second viewcontroller a reference to the first one before viewDidLoad is called. Do you use Segues ? So in prepare would be the right place. In the second viewcontroller you create a property for the first one and then use this property as target when you create the UITapGestureRecognizer.
Another way is using a protocol and delegation.
As the code shows, I have the switch which starts the animation when turned on, but the confetti anyway is displayed and doesn't let me have button interactions !
#IBAction func `switch`(_ sender: UISwitch) {
if(sender.isOn==true){
senderText.text="YAYA!"
supportText.text="we have added a reminder for you"
supportIcon.isHidden=false
support2.isHidden=true
confettiView.startConfetti()
}
else{
senderText.text="OKAY"
supportText.text="you wont be disturbed"
supportIcon.isHidden=true
support2.isHidden=false
}
}
var confettiView: SAConfettiView!
override func viewDidLoad() {
super.viewDidLoad()
confettiView = SAConfettiView(frame: self.view.bounds)
confettiView.colors = [UIColor(red:0.95, green:0.40, blue:0.27, alpha:1.0),
UIColor(red:1.00, green:0.78, blue:0.36, alpha:1.0),
UIColor(red:0.48, green:0.78, blue:0.64, alpha:1.0),
UIColor(red:0.30, green:0.76, blue:0.85, alpha:1.0),
UIColor(red:0.58, green:0.39, blue:0.55, alpha:1.0)]
confettiView.intensity = 0.5
confettiView.type = .Diamond
confettiView.type = .Confetti
view.addSubview(confettiView)
What is happening is that your confettiView is on top of all the other views and is receiving all the touches, preventing buttons and switches to receive them
confettiView.isUserInteractionEnabled = false
This line of code to be inserted right after your confettiView initialization should solve the issue, as the confetti view will no longer intercept all touches.
Another solution (depending on your design requirements) would be to bring all the buttons to front, if you have a reference for them, or to remove the confetti view if you want an animation of defined duration.
Also please not that
confettiView.type = .Diamond
confettiView.type = .Confetti
will cause the type to be .Confetti. Consider to remove one of the two type assign statement. :)
Your confettiView is above all of your other UIView's and UIButton's.
confettiView = SAConfettiView(frame: self.view.bounds) makes your confettiView the size of the entire screen. You're then adding it to the view with view.addSubview(confettiView). Remove the confettiView after your animation.
For example:
confettiView.removeFromSuperview()
I am using SWRevealViewController for side menu and IQKeyboardManagerSwift for keyboard.
When I am editing content in textfield and tries to open menu, the keyboard should automatically hide but I can't manage to do so.
How should this be done?
Capture the event of the opening menu. You can do this with SWRevealViewcontroller Delegate or by simply adding an #IBAction to the menu button.
In this method call .resignFirstResponder() for the element that needs the keyboard (like a textField):
textField.resignFirstResponder()
Of course you can call this function in every element that can have a keyboard to be sure to call the right one.
Since UIBarButtonItem doesn’t inherit from UIView or expose the item’s underlying view, this isn’t as easy as adding a gesture recognizer.
One plausible solution would be to [Step 1] define a custom view for the Side Menu Icon [Step 2]add the gesture to hide keyboard to it.
//gesture:tap anywehere to dismiss the keyboard
let tap = UITapGestureRecognizer(target:self.view,action:#selector(UIView.endEditing))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
let customButton = UIButton(frame: CGRect.init(x: 0, y: 0, width: 20, height: 20))
customButton.setImage(UIImage(named: "menu"), for: .normal)
//hide keyboard gesture(tap gesture)
customButton.addGestureRecognizer(tap)
customButton.isUserInteractionEnabled = true
if self.revealViewController() != nil {
customButton.addTarget(self.revealViewController(), action: #selector(SWRevealViewController.revealToggle(_:)), for: .touchUpInside)
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
self.navigationItem.leftBarButtonItem?.customView = customButton
Please accept as answer if it works.(Worked for me)