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.
Related
I'm stuck with some funny problem and ran out of ideas how to solve it.
In one of my controllers I use a simple scheme of adjusting a view frame according to the keyboard appearance.
In UITextFieldDelegate method I initialise the controller's property firstResponder:
func textFieldDidBeginEditing(_ textField: UITextField) {
self.firstResponder = textField
}
Then I use UIKeyboard notifications selectors to change the frame of contentView:
override func keyboardWillShow(_ notifications: Notification) {
super.keyboardWillShow(notifications)
let info = notifications.userInfo
let keyboardFrame:CGRect = (info![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
var bottomY:CGFloat!
if self.firstResponder == self.emailTextField{
bottomY = self.emailBottomLine.frame.origin.y + 80 + self.headerView.frame.height
}
else {
return
}
if bottomY >= keyboardFrame.origin.y {
let offset = bottomY - keyboardFrame.origin.y
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = -offset
})
}else{
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
}
override func keyboardWillHide(_ notifications: Notification) {
super.keyboardWillHide(notifications)
let info = notifications.userInfo
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
And everything works fine until I start typing inside the emailTextField. Each tap on the keyboard causes the contentView reset to its original position without animation.
The question is what really causes this behavior? I'm totally confused and have checked whatever thing is possibly affects this. Please, help!!!
Make sure either:
A. Your contentView does not have any layout constraints attached to it, or else when you set its frame, its frame will be reset on the next layout pass back to what the constraints say the frame should be.
or:
B. Use a constraint to position your contentView's vertical offset relative to the keyboard instead of adjusting its frame.
I am dealing with the problem of moving the view when the keyboard covers an element that just gained the first responder. I started by looking at this question and it gave me a great head start.
After adding the observers to UIKeyboardWillShow and UIKeyboardWillHide I ended with the following code:
func keyboardWillShow(notification: Notification) {
guard let userInfo = notification.userInfo,
let kbRect = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue,
let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double else {
return
}
let kbSize = kbRect.cgRectValue.size
UIView.animate(withDuration: duration) {
self.view.transform = CGAffineTransform(translationX: 0, y: -kbSize.height)
}
}
func keyboardWillHide(notification: Notification) {
UIView.animate(withDuration: 0.3) {
self.view.transform = CGAffineTransform(translationX: 0, y: 0)
}
}
It works fine in the sense that the view moves up and down when a text field gains the first responder.
However, when a key is pressed, the view moves again and defeats the purpose of moving it up on the first place.
I made a little GIF to better describe this undesired behavior, the first time the keyboard appears and disappears shows the correct behavior, the second time, when a key is pressed, shows the undesired one.
So, the question is, is there a way I could prevent the view movement when a key is pressed? I would like the view to stay "up" when the user is using the keyboard to insert text.
You should move view app, not transform it, use:
self.view.frame.origin.y -= kbSize.height
transforming should behave like it is right now
Set your UIView as a Outlet and insert this code in viewDidLoad
yourView.translatesAutoresizingMaskIntoConstraints = true
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
}
I am trying to move my view up only if certain textField is selected. I got it working, however, if I now select other textField, it activates again on other textFields also, why?
Like this I am dealing with the moving:
func keyboardWillShow(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
}
}
}
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
}
}
}
And like this I am trying to addObserver in textField touchDown:
#IBAction func didTapCertainTextField(_ sender: Any) {
NotificationCenter.default.addObserver(self, selector: #selector(CreateCardViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
}
Flow in words:
Lets say I have 5 textFields(1, 2, 3, 4, 5)
I add the observer to third textField. I click first, view doesn't move, I click third it moves, I click 1 again and now it moves. Why? I do not want the view to move if clicked textField is 1.
What you are looking for is to know if the textField is first responder, but what you are doing is getting notified when the keyboard is displayed.
I encourage you to look into this class of cocoa:
https://developer.apple.com/reference/uikit/uiresponder
Or you can in the precise cas of you UITextField bind actions to EditingDidBegin and EditingDidEnd through you the Xcode interface builder
or do it programmatically with a delegate
https://developer.apple.com/reference/uikit/uitextfielddelegate
Also I can notice that you start observing this events but it seems to me that you never stop observing them.
For that you should call removeObserver:name:object: at some point before the end of your object's life time
If I understand you correctly, you're trying to move the view up only when the keyboard is hiding the UITextField. If that is the case, I wrote the following extension specifically for that:
extension UIViewController {
func pushViewForTextFields() {
// Push view according text field's position
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillShow(_ sender: Notification) {
var currentEditingTextField: UITextField!
let keyboardHeight = UIScreen.main.bounds.maxY - ((sender as NSNotification).userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
// Find which text field is currently editing (O(n))
for view in self.view.subviews as [UIView] {
if let tf = view as? UITextField {
if tf.isEditing {
currentEditingTextField = tf
break
}
}
}
if currentEditingTextField == nil || currentEditingTextField < 0 {
// no textfield found
return
}
// get absolute frame of the text field, regardless of its parent view
let globalFrame = (currentEditingTextField.superview?.convert(senderObject.frame, to: self.view))!
// the 30 below is completely arbitrary; change it to whatever you want to suit your needs. This is basically the distance you want between the top of the keyboard and the editing text field (30px)
if globalFrame.maxY + 30 > keyboardHeight {
self.view.frame.origin.y = -(globalFrame.maxY + 30 - keyboardHeight)
}
else { // keyboard not covering the text field
self.view.frame.origin.y = 0
}
}
#objc func keyboardWillHide(_ sender: Notification) {
// move view back to normal position when editing is done
self.view.frame.origin.y = 0
}
}
Then simply use it in your UIViewController like this
override func viewDidLoad() {
super.viewDidLoad()
self.pushViewForTextFields()
}
Note that this extension only works if your UITextField objects are on the top level of the main UIView. i.e if they're not embedded in any subViews. If you have any text field in a subview, then you'll have to expand the for-loop by nesting another for-loop in there to check for every view in every subview (O(n^2) in this case).
This may not be the most efficient way (with looping over the UIView's tree and all), but I only wrote it to have a generic solution that I can easily apply to all my UIViewControllers
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.