How to solve keyboard problems in Swift 3? - ios

The problem is that when I try to write in the text fields the keyboard cover them up. How can I scroll the text field up to see what am I writing. I have below lines of code to enable the return key and to hide the keyboard when you touch in a different place:
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.view.endEditing(true)
return false
}

How can I scroll the text field up to see what am I writing
This can be achieved in two ways:
You can manage your frame programmatically on keyboard hide and show like #Taichi Kato
You can integrate libraries which serves the same purpose. One such library is IQKeyBoardManager with its Swift variant as IQKeyboardManagerSwift You can find it on GitHub and cocoapods
To achieve follow the below steps :
SWIFT 3
Just install IQKeyboardManagerSwift from Github or cocoapods
Import IQKeyboardManagerSwift in Appdelegate
Add the below lines of code in AppDelegate didFinishLaunchingWithOptions method.
IQKeyboardManager.sharedManager().shouldResignOnTouchOutside = true;
Objective-C
Install IQKeyBoardManager via any medium
Import #import "IQKeyboardManager.h" in Appdelegate
Add the below lines of code in AppDelegate didFinishLaunchingWithOptions method.
IQKeyboardManager.sharedManager.shouldResignOnTouchOutside = true;
And this is Done. This is the only code which you need to write.

The easiest solution to this problem is to put all the elements into one scrollview and then add the keyboard height to the constant of the bottom of the view to superview.
When the keyboard is shown or hidden, iOS sends out the following notifications to any registered observers:
UIKeyboardWillShowNotification
UIKeyboardDidShowNotification
UIKeyboardWillHideNotification
UIKeyboardDidHideNotification
So here is what you can do:
Get the size of the keyboard.
Adjust the bottom content inset of
your scroll view by the keyboard height.
Scroll the target text
field into view.
Something like this:
func keyboardWillShow(notification: NSNotification) {
print("KEYBOARD WILL SHOW")
let userInfo:NSDictionary = notification.userInfo! as NSDictionary
let keyboardFrame:NSValue = userInfo.value(forKey: UIKeyboardFrameEndUserInfoKey) as! NSValue
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
bottomConstraint.constant = keyboardHeight + 8
UIView.animate(withDuration: 0.5, animations: { [weak self] in
self?.view.layoutIfNeeded() ?? ()
})
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
bottomConstraint.constant = 8
UIView.animate(withDuration: 0.5, animations: { [weak self] in
self?.view.layoutIfNeeded() ?? ()
})
}

You can use this two framework for keyboard problem solved:
IQKeyboardManager : Link
iOSUtilitiesSource: Link

Related

ScrollView is not working properly with showing/hiding keyboard

Step 1: I have a textfield which is embedded in a scrollview, when I start editing the textfield, keyboard appears and I am changing scrollview insets accordingly.
Step 2: while keyboard is active I presented a viewcontroller, and came back.
step 3: Now if I start editing textfield again, the scrollview is stuck and not moving up as it was earlier.
After you add observers with two selectors keyboardWillShow and keyboardWillShow to the NotificationCenter.default you can try this
func keyboardWillShow(_ notification: NSNotification) {
super.keyboardWillShow(notification)
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
scrollView.contentInset.bottom = keyboardSize.height
}
}
func keyboardWillHide(_ notification: NSNotification) {
super.keyboardWillHide(notification)
scrollView.contentInset.bottom = 0
}

iOS Swift - Multiple UITextFields moving UIView

First I will describe the layout:
I have a UIView with two UITextfields. When I select either of the textfields I want the UIView to move up such that the textfields are not covered by the keyboards. The normal solution is obvious and already implemented: keyboardWillHide and keyboardWillShow. When i select one textfield the UIView behaves as expect, HOWEVER when I have one textfield selected and then the next textfield is selected the UIVIEW snaps back to the original constraints, and will not readjust, even when keyboardWillShow is called again.
How can i achieve the desired effect: When a textfield is selected the UIView moves up, then when the next textfield is selected the UIView remains in the exact same raised position.
Why does the UIView reset on the second textfield being selected currently?
Below is the relevant code, these functions are setup in the VDL. No other code touches the textfields. It is worth mentioning these textfields occur in a modal view over current context. Also worth mentioning the keyboards are of type decimalPad
// MARK: - keyboard Controls
func keyboardWillShow(notification:NSNotification) {
print("Keyboard show")
if isKeyboardOffset == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
print("Keyboard show... \(keyboardSize)")
self.viewToMove.frame.origin.y -= keyboardSize.height / 2
}
isKeyboardOffset = true
}
}
func keyboardWillHide(notification:NSNotification) {
print("Keyboard hide")
if isKeyboardOffset == true {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
print("keyboard hide...")
self.viewToMove.frame.origin.y += keyboardSize.height / 2
}
isKeyboardOffset = false
}
}
EDIT ANSWER: As stated in accepted answer Instead of adjusting the location of the UIView we choose to update the layout constraint dictating the UIViews location. The following implementation of keyboardWillShow
func keyboardWillShow(notification:NSNotification) {
if isKeyboardOffset == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
print("Keyboard show... \(keyboardSize)")
self.topConstraint.constant -= 100
}
isKeyboardOffset = true
}
}
Since you are using Auto Layout with constraints on your view it automatically gets reset back to the original position. So instead of changing the view position if you change the value of the constraint this should work.

Move view when keyboard displayed over UITextField

I have looked around and found this post about moving a view when a keyboard appears. It works great and moves the keyboard anytime I click in a UITextField. The issue is that I have three UITextFields, and the keyboard should only move when it is going to present over a UITextField. I looked at the Apple documentation on this as well, and found some useful information but I am still not getting the desired functionality.
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
var aRect = self.view.frame;
aRect.size.height -= keyboardSize.size.height
if self.view.frame.origin.y == 0{
if aRect.contains(activeField.frame.origin){
self.view.frame.origin.y -= keyboardSize.height
}
}
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
activeField = textField
}
func textFieldDidEndEditing(_ textField: UITextField) {
activeField = nil
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
}
From the Apple documentation I just took the piece where I create the aRect, and then check if the points intersect with the contains function. I would expect this to then make the view move only when the keyboard were to overlap with a textfield, and keep the view in place otherwise. For some reason that I don't fully understand, this is not the case. The keyboard will move the view in the case where any textfield is clicked (even though for some it shouldn't). I have played around with it a bit now and tried debugging but have been unsuccessful. Any ideas?
EDIT: I did a little debugging and it seems that the aRect.contains(...) is returning true for when all textfields are clicked, but in reality it should not. Is contains the right method to be using?
I followed this way to manage such issue in TableView same way you can manage in your view Here is step by step code:
within viewDidLoad added registerForKeyboardNotifications()
Here is the method
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWasShown:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}
Again define other method :
func keyboardWasShown(aNotification: NSNotification) {
let info = aNotification.userInfo as! [String: AnyObject],
kbSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue().size,
contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height, right: 0)
electricalViewListTableview.contentInset = contentInsets
electricalViewListTableview.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
var aRect = self.view.frame
aRect.size.height -= kbSize.height
if let activeTF = activeField {
if !CGRectContainsPoint(aRect, activeTF.frame.origin) {
electricalViewListTableview.scrollRectToVisible(activeTF.frame, animated: true)
}
}
}
Keyboard Hiding Method :
func keyboardWillBeHidden(aNotification: NSNotification) {
let contentInsets = UIEdgeInsetsZero
electricalViewListTableview.contentInset = contentInsets
electricalViewListTableview.scrollIndicatorInsets = contentInsets
}
After this use UITextFieldDelegates method to keep track active textfield :
var activeField: UITextField?
func textFieldDidBeginEditing(textField: UITextField) {
self.activeField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
self.activeField = textField
}
Hope it helps!
You have two main issues with your keyboardWillShow code.
You are using the wrong key to get the keyboard frame. You need UIKeyboardFrameEndUserInfoKey, not UIKeyboardFrameBeginUserInfoKey. You want to know where the keyboard will end up, not where it starts from.
Once you get the keyboard's frame, you need to convert it to local coordinates. It is given to you in screen coordinates.
Your updated code would be:
func keyboardWillShow(notification: NSNotification) {
if let keyboardScreenFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardLocalFrame = self.view.convert(keyboardScreenFrame, from: nil)
var aRect = self.view.frame;
aRect.size.height -= keyboardLocalFrame.size.height
if self.view.frame.origin.y == 0 {
if aRect.contains(activeField.frame.origin) {
self.view.frame.origin.y -= keyboardSize.height
}
}
}
}
You also have a big problem with your keyboardWillHide method. Since you keyboardWillShow method always shortens your view's frame, your keyboardWillHide method also needs to always restore the view's frame height.
If I were you, I wouldn't change the view's frame height in either method. Just adjust its origin as needed to make the text field visible.
Try IQKeyboardManager . It automatically manages text fields to make them visible. You just need to add it to your project, and no need to write even one line of code. A piece from it's documentation:
Often while developing an app, We ran into an issues where the iPhone
keyboard slide up and cover the UITextField/UITextView.
IQKeyboardManager allows you to prevent issues of the keyboard sliding
up and cover UITextField/UITextView without needing you to enter any
code and no additional setup required. To use IQKeyboardManager you
simply need to add source files to your project.
EDIT: In addition to Rmaddy's answer, I can say you should consider changing if aRect.contains(activeField.frame.origin) to if !aRect.contains(activeField.frame), because the first check will return true even if the top of your textfield is in the frame of the view, and also, you should be checking if it doesn't contain the frame of your textfield, then change the frame of the view.
And, I'm not totally sure, but, maybe it would be better if you move your activeField = textField code to the textFieldShouldBeginEditing delegate method.

Event on tapping out of a UITextView (i.e. UITextView losing focus)

I'd like to perform some processing on my UITextView once the user has finished editing it and tapped somewhere else on the screen. What's the best practice?
I almost managed to get the desired effect with func textViewDidEndEditing(textView: UITextView) however this only runs when the user has tapped 'Enter' key on the keyboard (which people very rarely do - they just commit the changes by tapping on somewhere else on the screen.)
The problem with func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) is that it doesn't care whether the UITextView was edited or not.
I think you can add an UITapGestureRecognizer on the view which hold the textView. In the UITapGestureRecognizer's selector, you can add the logic codes to handle the process. You can check the length of the textView's text to determine the UITextView was edited or not. Here is some sample code:
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGestureRecognizer)
}
func handleTap() {
let text = textView.text
textView.resignFirstResponder() // loosing focus
if text.characters.count > 0 {
// textView edited
} else {
// textView not edited
}
}

Tap Gesture on animating UIView not working

I have a tap gesture on a UILabel who's translation is being animated. Whenever you tap on the label during the animation there's no response from the tap gesture.
Here's my code:
label.addGestureRecognizer(tapGesture)
label.userInteractionEnabled = true
label.transform = CGAffineTransformMakeTranslation(0, 0)
UIView.animateWithDuration(12, delay: 0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
label.transform = CGAffineTransformMakeTranslation(0, 900)
}, completion: nil)
Gesture code:
func setUpRecognizers() {
tapGesture = UITapGestureRecognizer(target: self, action: "onTap:")
}
func onTap(sender : AnyObject) {
print("Tapped")
}
Any ideas? Thanks :)
Note added for 2021:
These days this is dead easy, you just override hitTest.
How to detect touches in a view which is moving
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let pf = layer.presentation()!.frame
// note, that is in the space of our superview
let p = self.convert(point, to: superview!)
if pf.contains(p) { return self }
return nil
}
It's that easy
Related tip -
Don't forget that in most cases if an animation is running, you will, of course, almost certainly want to cancel it. So, say there's a "moving target" and you want to be able to grab it with your finger and slide it somewhere else, naturally in that use case your code in your view controller will look something like ..
func sliderTouched() {
if alreadyMoving {
yourPropertyAnimator?.stopAnimation(true)
yourPropertyAnimator = nil
}
etc ...
}
You will not be able to accomplish what you are after using a tapgesture for 1 huge reason. The tapgesture is associated with the frame of the label. The labels final frame is changed instantly when kicking off the animation and you are just watching a fake movie(animation). If you were able to touch (0,900) on the screen it would fire as normal while the animation is occuring. There is a way to do this a little bit different though. The best would be to uses touchesBegan. Here is an extension I just wrote to test my theory but could be adapted to fit your needs.For example you could use your actual subclass and access the label properties without the need for loops.
extension UIViewController{
public override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else{return}
let touchLocation = touch.locationInView(self.view)
for subs in self.view.subviews{
guard let ourLabel = subs as? UILabel else{return}
print(ourLabel.layer.presentationLayer())
if ourLabel.layer.presentationLayer()!.hitTest(touchLocation) != nil{
print("Touching")
UIView.animateWithDuration(0.4, animations: {
self.view.backgroundColor = UIColor.redColor()
}, completion: {
finished in
UIView.animateWithDuration(0.4, animations: {
self.view.backgroundColor = UIColor.whiteColor()
}, completion: {
finished in
})
})
}
}
}
}
You can see that it is testing the coordinates of the CALayer.presentationLayer()..That's what I was calling the movies. To be honest, I have still not wrapped my head completely around the presentation layer and how it works.
I was stuck on this problem for hours, and could not understand why the tapping did not work on an animated label which slides out of screen after 3 seconds delay.
Well said agibson007, about the animation works like a fake movie's playback, the 3-second delay controls the payback of the movie, yet the label's frame is changed as soon as the animation begins without a delay. So the tapping (which depends on the label's frame at its original position) would not work.
My solution was changing the 3-second delay to a timeout function like -
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
[weak self] in
self?.hideLabel()
}
So that keeps the tapping works during the delay, and allow animation runs inside the hideLabel() call after the delay.
If you want to see the animation, you need to put it in the onTap handler.
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTap(sender:))
gesture.numberOfTapsRequired = 1
label.addGestureRecognizer(gesture)
label.userInteractionEnabled = true
label.transform = CGAffineTransformMakeTranslation(0, 0)
UIView.animateWithDuration(12, delay: 3, options: [.allowUserInteraction], animations: { () -> Void in
label.transform = CGAffineTransformMakeTranslation(0, 900)
}, completion: nil)
func onTap(sender: AnyObject)
{
print("Tapped")
}
For your tap gesture to work, you have to set the number of taps. Add this line:
tapGesture.numberOfTapsRequired = 1
(I'm assuming that tapGesture is the same one you call label.addGestureRecognizer(tapGesture) with)
Below is a more generic answer based on the answer from #agibson007 in Swift 3.
This didn't solve my issue immediately, because I had additional subviews covering my view. If you have trouble, try changing the extension type and writing print statements for touchLocation to find out when the function is firing. The description in the accepted answer explains the issue well.
extension UIViewController {
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touchLocation = touch.location(in: self.view)
for subview in self.view.subviews {
if subview.tag == VIEW_TAG_HERE && subview.layer.presentation()?.hitTest(touchLocation) != nil {
print("[UIViewController] View Touched!")
// Handle Action Here
}
}
}
}

Resources