iOS keyboard dismissal with multiple UIGestureRecognizers - ios

In my main app view which contains both a UITextField and a UITableView, I have the "usual" code using a UITapGestureRecognizer to dismiss the virtual keyboard if a tap is detected outside of the keyboard while I'm editing the contents of the UITextField.
An added feature is that this is only enabled if the virtual keyboard is actually shown - I don't want "background taps" to cause editing to end if the virtual keyboard isn't visible, but nor do I want background taps to trigger their normal behaviour - they should be consumed if the virtual keyboard is currently showing.
override func viewDidLoad() {
...
tapper = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
NotificationCenter.default.addObserver(self, selector: #selector(keyboardShown), name:
NSNotification.Name(rawValue: "UIKeyboardDidShowNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardHidden), name:
NSNotification.Name(rawValue: "UIKeyboardDidHideNotification"), object: nil)
}
#IBAction func keyboardShown(_ sender: AnyObject) {
view.addGestureRecognizer(tapper!)
}
#IBAction func keyboardHidden(_ sender: AnyObject) {
view.removeGestureRecognizer(tapper!)
}
#IBAction func viewTapped(_ sender: AnyObject) {
view.endEditing(false)
}
This mostly works, except that the UITableView has interactive header cells which each also have a UITapGestureRecognizer attached.
The net result is that if I click on a header cell the gesture recognizer on that cell gets fired, and not the one on the parent view, and the keyboard doesn't get dismissed. If I click on the data cells instead, everything works fine.
If it matters, my UITableView has its own UIViewController subclass and is contained in a nested UIView - the table is too complicated to have that code in my main view controller.
How can I prevent the sub-view's gesture recognizers from handling these taps when the parent view's recognizer is attached so that the parent view can handle them instead?

I've implemented what I consider a "temporary" solution by also observing the virtual keyboard notifications in the UITableView's controller, tracking the keyboard visibility state, and then implementing this UIGestureRecognizerDelegate method on the header cells' gesture recognizer:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return !keyboardShowing
}
This duplicates a certain amount of functionality from the main view in the sub-view which really shouldn't need to know about the state of the keyboard. I'm still looking for a method that can be handled entirely from within the parent view.
EDIT - with thanks to #Tommy for the hint, I now have a better solution that removes any need to track the keyboard state in the sub-view.
My parent view no longer uses a UIGestureRecognizer, but instead uses a custom subclass of UIView to track touch events, and conditionally ignore them:
class KeyboardDismissingView: UIView {
private var keyboardVisible = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let r = super.hitTest(point, with: event) else { return nil }
var v : UIView! = r
while v != nil {
if v is UITextField {
return r
}
v = v.superview
}
if keyboardVisible {
self.endEditing(false)
return nil
}
return r
}
func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardShown), name:
NSNotification.Name(rawValue: "UIKeyboardDidShowNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardHidden), name:
NSNotification.Name(rawValue: "UIKeyboardDidHideNotification"), object: nil)
}
#IBAction func keyboardShown(_ sender: AnyObject) {
keyboardVisible = true
}
#IBAction func keyboardHidden(_ sender: AnyObject) {
keyboardVisible = false
}
override init (frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setup()
}
}

Related

How can selection be disabled in PDFView?

Displaying a PDFDocument in a PDFView allows the user to select parts of the document and perform actions e.g. "copy" with the selection.
How can selection be disabled in a PDFView while preserving the possibility for the user to zoom in and out and scroll in the PDF?
PDFView itself does not seem to offer such a property nor does the PDFViewDelegate.
You have to subclass PDFView, as such:
class MyPDFView: PDFView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
super.addGestureRecognizer(gestureRecognizer)
}
}
Just need to do is it will auto clear the selection and User will no longer long-press on PDF text.
class MyPDFView: PDFView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
self.currentSelection = nil
self.clearSelection()
return false
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
super.addGestureRecognizer(gestureRecognizer)
}
}
This below 2 lines need to add in canPerformAction()
self.currentSelection = nil
self.clearSelection()
For iOS 13, the above solution no longer works. It looks like they've changed the internal implementation of PDFView and specifically how the gesture recognizers are set up. I think generally it's discouraged to do this kind of thing, but it can still be done without using any internal API, here's how:
1) Recursively gather all subviews of PDFView (see below for the helper function to do this)
let allSubviews = pdfView.allSubViewsOf(type: UIView.self)
2) Iterate over them and deactivate any UILongPressGestureRecognizers:
for gestureRec in allSubviews.compactMap({ $0.gestureRecognizers }).flatMap({ $0 }) {
if gestureRec is UILongPressGestureRecognizer {
gestureRec.isEnabled = false
}
}
Helper func to recursively get all subviews of a given type:
func allSubViewsOf<T: UIView>(type: T.Type) -> [T] {
var all: [T] = []
func getSubview(view: UIView) {
if let aView = view as? T {
all.append(aView)
}
guard view.subviews.count > 0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
I'm calling the above code from the viewDidLoad method of the containing view controller.
I haven't yet found a good way to work this into a subclass of PDFView, which would be the preferred way for reusability and could just be an addition to the above NonSelectablePDFView. What I've tried so far is overriding didAddSubview and adding the above code after the call to super, but that didn't work as expected. It seems like the gesture recognizers are only being added at a later step, so figuring out when that is and if there's a way for the subclass to call some custom code after this happened would be a next step here.
With Swift 5 and iOS 12.3, you can solve your problem by overriding addGestureRecognizer(_:) method and canPerformAction(_:withSender:) method in a PDFView subclass.
import UIKit
import PDFKit
class NonSelectablePDFView: PDFView {
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
(gestureRecognizer as? UILongPressGestureRecognizer)?.isEnabled = false
super.addGestureRecognizer(gestureRecognizer)
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
As an alternative to the previous implementation, you can simply toggle UILongPressGestureRecognizer isEnabled property to false in the initializer.
import UIKit
import PDFKit
class NonSelectablePDFView: PDFView {
override init(frame: CGRect) {
super.init(frame: frame)
if let gestureRecognizers = gestureRecognizers {
for gestureRecognizer in gestureRecognizers where gestureRecognizer is UILongPressGestureRecognizer {
gestureRecognizer.isEnabled = false
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
You should note that this is not sufficient to disable text selecting, as there is a UITapAndHalfRecognizer as well – obviously a private Apple class - that also creates selections.
It is attached to the PDFDocumentView, which is another private implementation detail of PDFView, and which you can not replace with your own class implementation.

Dismiss keyboard in iOS

I looked at and tried multiple solutions for Swift 3, Xcode 8 but couldn't get any to work. I've tried:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
and also setting a text field input as first responder:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
pressureInput.resignFirstResponder()
}
I don't know if something from Xcode 8 to Xcode 9 that cause these methods to not work, or if I messed elsewhere. I have 9 text fields and they've all set delegate to self. Their tags are incremented to move on to the next text field on pressing return. Don't think that would affect it. Sorry, new at this! The code runs fine with either of those attempted functions, but they keyboard stays. I would just like to dismiss keyboard when touched outside of any text field.
first of all write this extension in any swift file
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
}
Than in viewDidLoad of that View only call in any view controller there are textFields.
self.hideKeyboardWhenTappedAround()
Swift 4, 5. I always use hide keyboard when tapped around and return button.
override func viewDidLoad() {
super.viewDidLoad()
hideKeyboardWhenTappedAround()
emailTextField.delegate = self // your UITextfield
passwordTextField.delegate = self // your UITextfield
}
// Hide Keyboard
extension EmailAutorization: UITextFieldDelegate {
// Return button tapped
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
// Around tapped
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(EmailAutorization.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
Here is Solution of Dismiss Keyboard and Move View Up on Keyboard Open : Swift 5
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(taped))
view.addGestureRecognizer(tap)
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
//This Method Will Hide The Keyboard
#objc func taped(){
self.view.endEditing(true)
}
#objc func KeyboardWillShow(sender: NSNotification){
let keyboardSize : CGSize = ((sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size)!
if self.view.frame.origin.y == 0{
self.view.frame.origin.y -= keyboardSize.height
}
}
#objc func KeyboardWillHide(sender : NSNotification){
let keyboardSize : CGSize = ((sender.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size)!
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
Did you tried to debug the program to see if the code stops in the function at all(with break point)? Usually this code should work...Check if those textFields are in the super view or in a child view and if they are maybe you should call self.childView.endEditing(true).
If you really work with multiple textFields maybe you should try IQKeyboardManager library. I use it in all my projects. You can find it here: https://github.com/hackiftekhar/IQKeyboardManager. Simple to use and with good support. Just install it trough cocoa pods, put IQKeyboardManager.sharedManager().enable = true in the AppDelegate and you're ready to go. :)
Are you sure that touchesBegan is being called? If you're sure, try adding self.resignFirstResponder() to your touchesBegan function. This tells your view controller that it's no longer the first responder and should dismiss the keyboard.
If not, what you'll want to do is create a UITapGestureRecognizer, add it to your view, and wire it to a function that calls self.resignFirstResponder().

Handle text input view movement with keyboard pan gesture in iOS Swift

I am developing an iOS app and
I have a messaging view where I want to handle this situation:
-> I have a input view at the bottom of the view, that needs to be visible all the time except for some criteria where user is blocked/restricted to send message.
-> when the input view is focused, keyboard appears, I want to move the view along with keyboard frame.
-> I want keyboard to dismiss interactively with table view scrolling. With this being said, the view should respond to keyboard pan gesture and move along with as well
-> I tried using input accessory view but problem with that was when keyboard gets dismissed with table view scrolling, input view gets dismissed as well.
-> I also tried using willShow/willHide/willChangeFrame observers but with this, response is not to the point and it doesn't respond to keyboard interactive dismissal.
Anybody got solution to this...
Thanks for your time.
Swift 3+:
I have take a view into textview background and set the constraint of view (leading, trailing, bottom, fixed height). Create a #IBOutlet for bottom constraint and manage that below code:
class ViewController: UIViewController {
#IBOutlet var bottomConstraint: NSLayoutConstraint!
#IBOutlet var view_TextViewBg: UIView!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidShow(_:)),
name: NSNotification.Name.UIKeyboardWillShow,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidHide(_:)),
name: NSNotification.Name.UIKeyboardWillHide,
object: nil)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
// tap.delegate = self
view.addGestureRecognizer(tap)
}
func handleTap(sender: UITapGestureRecognizer? = nil) {
//dissmiss your keyboard here
}
//MARK: Keyboard show
func keyboardDidShow(_ notification: Notification) {
let params = notification.userInfo
let rect: CGRect? = (params?[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue
bottomConstraint.constant = (rect?.size.height)!
}
//MARK: Keyboard hide
func keyboardDidHide(_ notification: Notification) {
bottomConstraint.constant = 0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Calling function when user taps anywhere on screen

In my app I have a small menu I made which is basically a UIView with two button on it. The menu opens when the user taps a button and closes also when the user taps the same button. I'd like the menu to close when the user taps anywhere outside of the menu UIView.
The menu:
You can also apply this easy way
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapBlurButton(_:)))
self.view.addGestureRecognizer(tapGesture)
func tapBlurButton(_ sender: UITapGestureRecognizer) {
if //checkmenuopen
{
closemenuhere
}
}
For that when you show the small menu, add below it a invisible button (UIColor.clear) with the entire screen as a frame. And it's action is to dismiss the menu of yours.
Make sure when you dismiss the small menu to dismiss thus button as well.
Hope this helps!
You can use basically touches began function
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("TAPPED SOMEWHERE ON VIEW")
}
There are several solutions to your case:
1- Implementing touchesBegan(_:with:) method in your ViewController:
Tells this object that one or more new touches occurred in a view or
window.
Simply, as follows:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// do something
}
2- Add a UITapGestureRecognizer to the main view of your ViewController:
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doSomething(_:)))
view.addGestureRecognizer(tapGesture)
}
func doSomething(_ sender: UITapGestureRecognizer) {
print("do something")
}
Or -of course- you could implement the selector without the parameter:
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doSomething))
view.addGestureRecognizer(tapGesture)
}
func doSomething() {
print("do something")
}
3- You could also follow Mohammad Bashir Sidani's answer.
I would suggest to make sure add the appropriate constraints to your button whether it has been added programmatically or by storyboard.
I'm not sure the code below will work in your case, just a advice.
class ViewController: UIViewController {
var closeMenuGesture: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
closeMenuGesture = UITapGestureRecognizer(target: self, action: #selector(closeMenu))
closeMenuGesture.delegate = self
// or closeMenuGesture.isEnable = false
}
#IBAction func openMenu() {
view.addGestureRecognizer(closeMenuGesture)
// or closeMenuGesture.isEnabled = true
}
#IBAction func closeMenu() {
view.removeGestureRecognizer(closeMenuGesture)
// or closeMenuGesture.isEnabled = false
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.view === self.view // only valid outside menu UIView
}
}
And I never be in this situation so not sure making enable/disable closeMenuGesture is enough to ensure other controls work normally, or to add/remove closeMenuGesture is more insured.

iOS 8 - Keyboard changes colour if you press Home button on iPad and come back in

To replicate:
Create a blank single-view project
Drag a TextField on to the canvas
Set the TextField keyboardAppearance to Dark
Run the app on iPad (device or simulator)
Touch the TextField to bring up the keyboard (it is dark)
Press Home, then come back into the app
Notice the keyboard changes colour (to white).
Presumably the keyboard colour changes to match the background. However in this case some of the keys remain dark, so this seems like a bug in iOS (see attached screenshot).
Anyone care to shed any light on this? We're using a workaround which involves hiding the keyboard and re-showing it, but this isn't ideal.
This code can close the keyboard when Home button is pressed and bring it back when the app re-starts. You need to set UITextFields delegate to the view controller:
class ViewController: UIViewController, UITextFieldDelegate {
private var _textField: UITextField!
private var _isFirstResponder: Bool!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "didBecomeActiveNotification:", name: UIApplicationDidBecomeActiveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "willResignActiveNotification:", name: UIApplicationWillResignActiveNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func didBecomeActiveNotification(nofication: NSNotification) {
if _isFirstResponder? == true {
_textField?.becomeFirstResponder()
}
}
func willResignActiveNotification(nofication: NSNotification) {
if _textField?.isFirstResponder() == true {
_isFirstResponder = true
_textField?.resignFirstResponder()
} else {
_isFirstResponder = false
}
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
_textField = textField
return true
}
}

Resources