I have created an extension of UIScrollView where when a user selects a textfield and the keyboard appears, the textfield will scroll up if it is in the way of the keyboard. I have it working for a UITextField but it doesn't seem to work with a UITextView. I have searched many posts on stackoverflow but can't seem to find anything to help. Here is the code for the extension:
extension UIScrollView {
func respondToKeyboard() {
self.registerForKeyboardNotifications()
}
func registerForKeyboardNotifications() {
// Register to be notified if the keyboard is changing size i.e. shown or hidden
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(keyboardWasShown(_:)),
name: UIKeyboardWillShowNotification,
object: nil
)
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(keyboardWillBeHidden(_:)),
name: UIKeyboardWillHideNotification,
object: nil
)
}
func keyboardWasShown(notification: NSNotification) {
if let info = notification.userInfo,
keyboardSize = info[UIKeyboardFrameBeginUserInfoKey]?.CGRectValue.size {
self.contentInset.bottom = keyboardSize.height + 15
self.scrollIndicatorInsets.bottom = keyboardSize.height
var frame = self.frame
frame.size.height -= keyboardSize.height
}
}
func keyboardWillBeHidden(notification: NSNotification) {
self.contentInset.bottom = 0
self.scrollIndicatorInsets.bottom = 0
}
In my view controller I would just set it like:
scrollView.respondToKeyboard()
Can someone point me in the right direction of how I can implement the UITextView as an extension to move up if the keyboard is in the way?
You can try using the UITextView delegate methods. Check out this link for more details. For swift, check the tutorial here.
for me this solution works fine for UITextView. Perhaps u can update this for Scrollview
// keyboard visible??
lazy var keyboardVisible = false
// Keyboard-Height
lazy var keyboardHeight: CGFloat = 0
func updateTextViewSizeForKeyboardHeight(keyboardHeight: CGFloat) {
textView.contentInset.bottom = keyboardHeight
self.keyboardHeight = keyboardHeight
}
func keyboardDidShow(notification: NSNotification) {
if let rectValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue {
if keyboardVisible == false {
let keyboardSize = rectValue.CGRectValue().size
keyboardVisible = true
updateTextViewSizeForKeyboardHeight(keyboardSize.height)
}
}
}
func keyboardDidHide(notification: NSNotification) {
if keyboardVisible {
keyboardVisible = false
updateTextViewSizeForKeyboardHeight(0)
}
}
Related
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
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
implement textFieldShouldBeginEditing in the delegate to set your active text field, instead of textFieldDidBeginEditing (make sure you return true from textFieldShouldBeginEditing to allow editing)
use keyboardDidShowNotification instead of keyboardWillShowNotification.
This will ensure you have your UITextField marked before getting the keyboard frame / details.
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
}
}
You need to define tag property to your textFields and check in
textFieldDidBeginEditing and textFieldDidEndEditing what UITextField was called.
I use code below to move screen up and down when keyboard show and hide
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector:#selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc 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
}
}
}
#objc 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
}
}
}
It works in all devices except iPhone x (I think the problem is iPhone X bottom wasted space)
Problem is that keyboard size change before shown and after that and cause the view go down and down and down...
Can anyone help?
You should also consider safe area insets
let endFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect.zero
let bottomSpace = keyboardValues.endFrame.maxY - keyboardValues.endFrame.minY - self.view.safeAreaInsets.bottom
And you should not be additive. These notifications may be posted multiple times. Use absolute values.
Here it is,
Use IQKeyboardManagerSwift
AppDelegate.swift settings
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.enableAutoToolbar = false
In My ViewController
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
print(#function)
if DeviceType.checkiPhoneX() {
IQKeyboardManager.shared.keyboardDistanceFromTextField = 40.0
}
return true
}
func textViewDidEndEditing(_ textView: UITextView) {
if DeviceType.checkiPhoneX() {
IQKeyboardManager.shared.keyboardDistanceFromTextField = 0.0
}
}
This is how i manage Keyboard for X.
Hope this may help you.
Use UIKeyboardFrameEndUserInfoKey instead of UIKeyboardFrameBeginUserInfoKey.
I have an autoscroll functionality of the tableView based on the UITextView cursor location inside the cell when editing.
It worked in the previous iOS versions. Starting from iOS11 it is broken.
I have set the tableView contentInset based on the keyboard height. For autoscrolling am using following code in textViewDidChange
if let confirmedTextViewCursorPosition = textView.selectedTextRange?.end {
let caretPosition = textView.caretRect(for: confirmedTextViewCursorPosition)
var textViewActualPosition = tableView.convert(caretPosition, from: textView.superview?.superview)
textViewActualPosition.origin.y += 22.0
tableView.scrollRectToVisible(textViewActualPosition, animated: false)
}
Askh1t is correct, here is my implementation:
var info = notification.userInfo!
var keyboardSize:CGRect = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
if keyboardSize.size.height <= 0 { // to fix bug on iOS 11
keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
}
as well as the full modular implementation that should work for you:
//MARK: - Properties
var activeTextView: UITextView?
//MARK: - Scroll View Notifications
// add in viewDidLoad
func registerForKeyboardNotifications(){
//Adding notifies on keyboard appearing
NotificationCenter.default.addObserver(forName: Notification.Name.UIKeyboardWillShow, object: nil, queue: nil, using: keyboardWasShown)
NotificationCenter.default.addObserver(forName: Notification.Name.UIKeyboardWillHide, object: nil, queue: nil, using: keyboardWillBeHidden)
}
//add in viewWillDisappear
func deregisterFromKeyboardNotifications(){
//Removing notifies on keyboard appearing
NotificationCenter.default.removeObserver(self, name: Notification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.removeObserver(self, name: Notification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWasShown(notification: Notification) -> Void {
//Need to calculate keyboard exact size due to Apple suggestions
self.tableView.isScrollEnabled = true
var info = notification.userInfo!
var keyboardSize:CGRect = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
if keyboardSize.size.height <= 0 { // to fix bug on iOS 11
keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
}
self.tableView.contentInset.bottom = keyboardSize.height //add this much
self.tableView.scrollIndicatorInsets.bottom = keyboardSize.height //scroll too it.
var aRect : CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
if let activeField = self.activeTextView {
if (!aRect.contains(activeField.frame.origin)){
self.tableView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}
func keyboardWillBeHidden(notification: Notification){
self.tableView.contentInset.bottom = 0
self.tableView.isScrollEnabled = true
self.tableView.alwaysBounceVertical = true
}
func textViewDidBeginEditing(_ textView: UITextView){
activeTextView = textView
}
func textViewDidEndEditing(_ textView: UITextView){
tableView.isScrollEnabled = true
activeTextView = nil
}
I'm currently working on an app for iPhone using Swift 3 however I have ran into an issue with the scrollview.
Prior to selecting a text field and having the keyboard appear, the scrollview work normally (ie.: I can scroll up and down), however after dismissing the keyboard, the scrollview does not allow me to scroll anymore which is my problem.
Note: if I select a text field again and make the keyboard appear it works fine and stops working once it is dismissed again.
I have checked the isScrollEnabled property of the scrollview and it appears to be enabled. Unfortunately I am still not too familiar with all the details of the scrollview and cannot seem to figure out why it has stopped working.
Any help or pointers as to where I could look would be greatly appreciated.
Edit: there is quite a bit of code but here is the narrowed down portion related to scroll view and keyboard:
class ScrollViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate {
//Scroll view
#IBOutlet weak var scrollView: UIScrollView!
//UIView inside the scroll view
#IBOutlet weak var contentView: UIView!
//Save button on the top right corner
#IBOutlet weak var saveButton: UIBarButtonItem!
//Text field being editted
var activeTextField:UITextField?
fileprivate var contentInset:CGFloat?
fileprivate var indicatorInset:CGFloat?
override func viewDidLoad() {
contentInset = scrollView.contentInset.bottom
indicatorInset = scrollView.scrollIndicatorInsets.bottom
NotificationCenter.default.addObserver(self,
selector: #selector(ScrollViewController.keyboardWillShow(_:)),
name: NSNotification.Name.UIKeyboardWillShow,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(ScrollViewController(_:)),
name: NSNotification.Name.UIKeyboardWillHide,
object: nil)
}
func adjustInsetForKeyboardShow(_ show: Bool, notification: Notification) {
let userInfo = notification.userInfo ?? [:]
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let adjustmentHeight = (keyboardFrame.height + 20) * (show ? 1 : -1)
scrollView.contentInset.bottom = (contentInset! + adjustmentHeight)
scrollView.scrollIndicatorInsets.bottom = (indicatorInset! + adjustmentHeight)
}
func keyboardWillShow(_ notification: Notification) {
adjustInsetForKeyboardShow(true, notification: notification)
}
func keyboardWillHide(_ notification: Notification) {
adjustInsetForKeyboardShow(false, notification: notification)
}
//Tap gesture to dismiss the keyboard
#IBAction func hideKeyboard(_ sender: AnyObject) {
self.view.endEditing(false)
}
deinit {
NotificationCenter.default.removeObserver(self);
}
}
I have create extension of UIViewController and create method for scrollView. Just need to call from viewWillAppear() and viewDidDisappear()
extension UIViewController {
func registerForKeyboradDidShowWithBlock (scrollview:UIScrollView ,block: ((CGSize?) -> Void)? = nil ){
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardDidShow, object: nil, queue: nil) { (notification) in
if let userInfo = (notification as NSNotification).userInfo {
if let keyboarRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.findFirstResponder() != nil {
let keyboarRectNew = self.view .convert(keyboarRect, to: self.view)
let scrollViewSpace = scrollview.frame.origin.y + scrollview.contentOffset.y
let textFieldRect:CGRect = self.view.findFirstResponder()!.convert(self.view.findFirstResponder()!.bounds, to: self.view)
let textFieldSpace = textFieldRect.origin.y + textFieldRect.size.height
let remainingSpace = self.view.frame.size.height - keyboarRectNew.size.height
if scrollViewSpace + textFieldSpace > remainingSpace {
let gap = scrollViewSpace + textFieldSpace - remainingSpace
scrollview .setContentOffset(CGPoint(x: scrollview.contentOffset.x, y: gap), animated: true)
}
}
}
}
block?(CGSize.zero)
}
}
func registerForKeyboardWillHideNotificationWithBlock ( scrollview:UIScrollView ,block: ((Void) -> Void)? = nil) {
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillHide, object: nil, queue: nil, using: { (notification) -> Void in
scrollview.scrollRectToVisible(CGRect(x: 0, y: 0, width: 0, height: 0), animated: true)
scrollview.contentOffset = CGPoint(x: 0, y: 0)
scrollview.contentInset = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0);
block?()
})
}
func deregisterKeyboardShowAndHideNotification (){
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
self.view.findFirstResponder()?.resignFirstResponder()
}
}
I have created extension of UIView to create find first responder method in that.
extension UIView {
func findFirstResponder() -> UIView? {
if self.isFirstResponder {
return self
}
for subview: UIView in self.subviews {
let firstResponder = subview.findFirstResponder()
if nil != firstResponder {
return firstResponder
}
}
return nil
}
}
Create Extension of UIView and write down this method.
extension UIView {
func findFirstResponder() -> UIView? {
if self.isFirstResponder {
return self
}
for subview: UIView in self.subviews {
let firstResponder = subview.findFirstResponder()
if nil != firstResponder {
return firstResponder
}
}
return nil
}
}
Their is a third party library in Objective C to handle the keyboard with scroll view, collection view and table view. The name of library is tpkeyboardavoidingscrollview. Try to embed this if possible.
I have a text view that sits at the bottom of my screen, and would like to push it up when users start editing the text view. I know how to push the view upwards, but because of different screen sizes the display isn't always the same. I'd like to get the height of the keyboard so that the text view sits just above it when a user edits. I have a piece of code below, yet when I add the following 3 lines, the app fails to build and gives me an error. Without this piece of code, the app runs perfectly. The error I get is "swift compiler error: expected declaration". These lines go into my keyboardWillShow function.
if let userInfo = sender.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
keyboardHeight = keyboardSize.height
}
So the full code looks like this.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name:UIKeyboardWillShowNotification, object: nil);
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name:UIKeyboardWillHideNotification, object: nil);
}
var isShown: Bool = false
var willHide: Bool = false
var keyboardHeight: CGFloat = 0
func keyboardWillShow(sender: NSNotification) {
isShown = true
//The following 3 lines seem to be the problem. Somehow the code stops working when I add them.
if let userInfo = sender.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
keyboardHeight = keyboardSize.height
}
}
func keyboardWillHide(sender: NSNotification) {
willHide = true
}
func textViewDidBeginEditing(textView: UITextView) {
if isShown == true {
self.view.frame.origin.y -= keyboardHeight
}
}
func textViewDidEndEditing(textView: UITextView) {
if willHide == true {
self.view.frame.origin.y += keyboardHeight
}
}