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
Related
This is my view controller.
I am trying to move the textFields up while the keyboard is covering textFields of the bottom of the screen. Here is the code I've done :
override func viewDidLoad() {
super.viewDidLoad()
txtFName.delegate = self
txtLName.delegate = self
txtCompany.delegate = self
txtStreet1.delegate = self
txtStreet2.delegate = self
txtTown.delegate = self
txtPin.delegate = self
txtPhone.delegate = self
txtEmail.delegate = self
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyBoardWillChange(notification:)),
name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyBoardWillChange(notification:)),
name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyBoardWillChange(notification:)),
name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
deinit {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
print("Return Tapped")
txtFName.resignFirstResponder()
txtLName.resignFirstResponder()
txtCompany.resignFirstResponder()
txtStreet1.resignFirstResponder()
txtStreet2.resignFirstResponder()
txtTown.resignFirstResponder()
txtPin.resignFirstResponder()
txtPhone.resignFirstResponder()
txtEmail.resignFirstResponder()
view.frame.origin.y = 0
return true
}
#objc func keyBoardWillChange(notification: Notification) {
print("Keyboard will show: \(notification.name.rawValue)")
view.frame.origin.y = -250
}
Now while I am tapping on any of the textFields, the whole view is moving up. "txtFName", "txtLName".. these textFields are not being visible.
I want to move up the view only when I would tap on "txtPin", "txtPhone", "txtEmail". Rest textfields would remain in the default position even when the keyboard appears.
what the required changes are?
Generally you can use your isFirstResponder property on your text field. When true, it means that this is the text field you wish to focus on and move upwards when it starts being edited.
#objc private func keyBoardWillChange(notification: Notification) {
let possibleTextFields: [UITextField] = [txtLName, txtCompany, txtStreet1, txtStreet2, txtTown, txtPin, txtPhone, txtEmail]
let focusedTextField: UITextField? = possibleTextFields.first { $0.isFirstResponder }
...
}
Now that you have that you would still need to calculate the offset that is needed to move your view
Getting the frame of keyboard:
guard let info = notification.userInfo else { return }
guard let value: NSValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let newFrame = value.cgRectValue
Computing the difference
There are tools to check frames which allow you to convert frames to different coordinate systems (views)
private func getVerticalDifference(keyboardFrame: CGRect, viewToFocus: UIView, panel: UIView) -> CGFloat {
let keyboardInPanel = panel.convert(keyboardFrame, from: nil)
let viewInPanel = panel.convert(viewToFocus.bounds, from: viewToFocus)
return viewInPanel.maxY - keyboardInPanel.minY
}
I suggest you use the view of your view controller for your panel parameter it should be something that is not being changed (Which is not the case in your current code. Avoid changing frame of the view of your view controller).
Applying the difference:
To apply the difference I suggest that you use constraints. Put all your text fields on a single "panel" view. This view may best also put into a scroll view so user may scroll through it on smaller devices. Now panel (or scroll view) can have a low priority (500 for instance) bottom constraint to view controller. Then another high priority bottom constraint set to greaterThan meaning that bottom will be fixed to "greater than X" where X can be setup later.
Now you can drag an outlet to your code from this greaterThan constraint. And then all you need to do is
bottomConstraintOutlet.constant = max(0.0, getVerticalDifference(...))
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
}
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.
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.
Previously if one presented a keyboard on one's own app one would embed everything in a UIScrollView and adjust the contentInset to keep content from being obscured by the keyboard.
Now with split view multitasking on iOS 9 the keyboard may appear at any moment and stay visible even while the user is no longer interacting with the other app.
Question
Is there an easy way to adapt all view controllers that were not expecting the keyboard to be visible and without start embedding everything in scrollviews?
The secret is to listen to the UIKeyboardWillChangeFrame notification that is triggered whenever the keyboard is shown/hidden from your app or from another app running side by side with yours.
I created this extension to make it easy to start/stop observing those events (I call them in viewWillAppear/Disappear), and easily get the obscuredHeight that is usually used to adjust the bottom contentInset of your table/collection/scrollview.
#objc protocol KeyboardObserver
{
func startObservingKeyboard() // Call this in your controller's viewWillAppear
func stopObservingKeyboard() // Call this in your controller's viewWillDisappear
func keyboardObscuredHeight() -> CGFloat
#objc optional func adjustLayoutForKeyboardObscuredHeight(_ obscuredHeight: CGFloat, keyboardFrame: CGRect, keyboardWillAppearNotification: Notification) // Implement this in your controller and adjust your bottom inset accordingly
}
var _keyboardObscuredHeight:CGFloat = 0.0;
extension UIViewController: KeyboardObserver
{
func startObservingKeyboard()
{
NotificationCenter.default.addObserver(self, selector: #selector(observeKeyboardWillChangeFrameNotification(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
func stopObservingKeyboard()
{
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
func observeKeyboardWillChangeFrameNotification(_ notification: Notification)
{
guard let window = self.view.window else {
return
}
let animationID = "\(self) adjustLayoutForKeyboardObscuredHeight"
UIView.beginAnimations(animationID, context: nil)
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: (notification.userInfo![UIKeyboardAnimationCurveUserInfoKey]! as AnyObject).intValue)!)
UIView.setAnimationDuration((notification.userInfo![UIKeyboardAnimationCurveUserInfoKey]! as AnyObject).doubleValue)
let keyboardFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue
_keyboardObscuredHeight = window.convert(keyboardFrame!, from: nil).intersection(window.bounds).size.height
let observer = self as KeyboardObserver
observer.adjustLayoutForKeyboardObscuredHeight!(_keyboardObscuredHeight, keyboardFrame: keyboardFrame!, keyboardWillAppearNotification: notification)
UIView.commitAnimations()
}
func keyboardObscuredHeight() -> CGFloat
{
return _keyboardObscuredHeight
}
}