0

I'm populating a vertical UIScrollView with many UITextField fields dynamically on runtime. The problem I have is that the keyboard will hide the fields that are in the area where it will appear, this may include the field I'm editing.

I tried KeyboardManagement solution from the apple documentation and also tried with notifications on the textFieldDidBeginEditing and textFieldDidEndEditing but the problem in both cases is that the keyboardWillShow notification comes first sometimes, and in that case it doesn't let me know which field is the one being edited.

I have this code in a class that implements the UITextFieldDelegate protocol, each object of this class holds a reference to one of those fields and works as it's delegate

func textFieldDidBeginEditing(_ textField: UITextField) {
    self.activeTextfield = self.valueTextField
}

func textFieldDidEndEditing(_ textField: UITextField) {
    self.activeTextfield = nil
}

The activeTextfield variable is a weak reference to the variable in the UIViewController where all of this happens. In that view controller I have the following code

class MyClass: UIViewController {
    var activeTextfield: CustomTextField! // This is the variable I was talking about on the previous paragraph

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        NotificationCenter.default.removeObserver(self)
    }

    @objc func keyboardWillShow(_ notification: Notification) {
        if self.view.frame.origin.y == 0 {
            guard let userInfo = notification.userInfo else { return }
            guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }

            let keyboardFrame = keyboardSize.cgRectValue
            let textFieldFrame = activeTextfield!.frame // activeTextfield sometimes is nil because this notification happens before the previous code block

            if textFieldFrame.origin.y + textFieldFrame.size.height > keyboardFrame.origin.y {
                self.view.frame.origin.y -= keyboardFrame.height
            }
        }
    }

    @objc func keyboardWillHide(_ notification: Notification) {
        if self.view.frame.origin.y != 0 {
            guard let userInfo = notification.userInfo else { return }
            guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }

            let keyboardFrame = keyboardSize.cgRectValue

            self.view.frame.origin.y += keyboardFrame.height
        }
    }
}

Is there any way I can force the UITextField delegate methods to be called before the keyboard notification?

Is this the correct way to handle this kind of situation?

If not, how should I handle it?

Thanks

Bhavesh Nayi
  • 3,626
  • 1
  • 27
  • 42
Carlos M.
  • 147
  • 9
  • 1
    It's not the best solution but it simple enough. Once keyboardWillShow notification fires you need to find firstResponder in your current view hierarchy. You can do it the following way: https://stackoverflow.com/questions/1823317/get-the-current-first-responder-without-using-a-private-api https://stackoverflow.com/a/14135456/6388105 – Dimitri L. Apr 26 '19 at 14:54

3 Answers3

2

As stated in your question:

the problem in both cases is that the keyboardWillShow notification comes first sometimes, and in that case it doesn't let me know which field is the one being edited

As per the sequence of events described in apple's documentation, textFieldShouldBeginEditing is the first delegate method called.

So, you can

  1. implement textFieldShouldBeginEditing in the delegate to set your active text field, instead of textFieldDidBeginEditing (make sure you return true from textFieldShouldBeginEditing to allow editing)
  2. use keyboardDidShowNotification instead of keyboardWillShowNotification.

This will ensure you have your UITextField marked before getting the keyboard frame / details.

Bhavesh Nayi
  • 3,626
  • 1
  • 27
  • 42
Swapnil Luktuke
  • 10,385
  • 2
  • 35
  • 58
1

You can do so fairly simply by doing the following. First add notification observers in your view will appear.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)        
    // Keyboard notification
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}

Then in your selector function you can have something like this

@objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
            let currentTextField = view.getSelectedTextField() {

        let keyboardHeight = keyboardSize.height
        let textFieldFrame = currentTextField.superview?.convert(currentTextField.frame, to: nil)            
        }
    }
}

And your getSelectedTextField() extension looks like this

// Inside UIView Extension 
// Get currently active textfield
func getSelectedTextField() -> UITextField? {

    let totalTextFields = getTextFieldsInView(view: self)

    for textField in totalTextFields{
        if textField.isFirstResponder{
            return textField
        }
    }
    return nil
}

    func getTextFieldsInView(view: UIView) -> [UITextField] {

        var totalTextFields = [UITextField]()

        for subview in view.subviews as [UIView] {
            if let textField = subview as? UITextField {
                totalTextFields += [textField]
            } else {
                totalTextFields += getTextFieldsInView(view: subview)
            }
        }
        return totalTextFields
    }
}
yambo
  • 1,388
  • 1
  • 15
  • 34
0

You need to define tag property to your textFields and check in textFieldDidBeginEditing and textFieldDidEndEditing what UITextField was called.

Bhavesh Nayi
  • 3,626
  • 1
  • 27
  • 42
Artyom Vlasenko
  • 385
  • 3
  • 16
  • All I need is the textfield object, which I get as a parameter of those methods, but I don't know how to send it to the ViewController before the keyboard notification (because in the method called for that notification, I decide if I should shift the screen up or not based on that textfield's coordinates) – Carlos M. Apr 26 '19 at 14:52
  • 1
    Why are you use self.view.frame.origin.y for screen up? You scrollView methods for this setContentOffset or scrollRectToVisible. – Artyom Vlasenko Apr 26 '19 at 14:58
  • To move the frame of the main UIView up – Carlos M. Apr 26 '19 at 15:05