Matt Hudson
Matt Hudson

Reputation: 7358

Scroll UITextField above Keyboard in a UITableViewCell on a regular UIViewController

I have tried most of the examples here on StackOverflow. I also used Apple's. The problem I seem to have with them is that they don't account for the UITextField being in a UITableView. I've done this many times, but not in this way. I have a custom UITableViewCell with a UITextField in it.

On my UITableView (which is NOT a UITableViewController), I need to be able to avoid hiding the UITextField under the UITableView.

I have this as of now:

-(void)viewDidLoad
{
....
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillShow:)
                                                 name:UIKeyboardWillShowNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillHide:)
                                                 name:UIKeyboardWillHideNotification
                                               object:nil];

....
}

- (void)scrollToRectOfTextField {
    UITableViewCell *cell = (UITableViewCell*)[self.activeTextField superview];
    CGRect r = CGRectMake(self.activeTextField.frame.origin.x,
                          cell.frame.origin.y+self.activeTextField.frame.origin.y,
                          self.activeTextField.frame.size.width,
                          self.activeTextField.frame.size.height);
    [self.tableView scrollRectToVisible:r animated:YES];
}

- (void)keyboardDidShow:(NSNotification *)notification
{
    if (UI_USER_INTERFACE_IDIOM()== UIUserInterfaceIdiomPhone) {
        NSDictionary *userInfo = [notification userInfo];
        CGSize size = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
        NSLog(@"TableView: %@", NSStringFromCGRect(self.tableView.frame));
        CGRect newTableViewFrame = CGRectMake(self.tableView.frame.origin.x,
                                              self.tableView.frame.origin.y,
                                              self.tableView.frame.size.width, self.tableView.frame.size.height - size.height);
        self.tableView.frame = newTableViewFrame;
        NSLog(@"New TableView: %@", NSStringFromCGRect(self.tableView.frame));


        self.tableView.contentSize = CGSizeMake(self.tableView.contentSize.width, self.tableView.contentSize.height-size.height);
    }
}


- (void)keyboardWillHide:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];
    CGSize size = [[userInfo objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    self.tableView.frame = CGRectMake(self.tableView.frame.origin.x,
                                      self.tableView.frame.origin.y,
                                      self.tableView.frame.size.width,
                                      self.tableView.frame.size.height + size.height);

    NSLog(@"%@", NSStringFromCGRect(self.tableView.frame));
}


-(void)textFieldDidBeginEditing
{
     [self scrollToRectOfTextField];
}

This seems to push a bunch of white space up past my keyboard. Please also note that I have an inputAccessoryView on my UITextField as well.

Oh, and I cannot use any third party classes. I love TPKeyboardAvoidingScrollView, but I cannot use anything third party.

Upvotes: 28

Views: 47325

Answers (11)

paderick
paderick

Reputation: 11

Converted @Matt Hudsons answer to Swift 4.2 and improved with a delegate:

func cell(_ cell: UITableViewCell, willStartEditing view: UIView) {
    if let inputAccessoryViewHeight = view.inputAccessoryView?.frame.size.height {
        var contentOffset = self.tableView.contentOffset
        let pointInTable = view.convert(view.frame.origin, to: self.tableView)
        
        contentOffset.y = pointInTable.y - inputAccessoryViewHeight
        
        self.tableView.setContentOffset(contentOffset, animated: true)
    }
}

func cell(_ cell: UITableViewCell, willEndEditing view: UIView) {
    view.resignFirstResponder()
    
    if let position = self.tableView.indexPath(for: cell) {
        self.tableView.scrollToRow(at: position, at: .middle, animated: true)
    }
}

Implement the UITextFieldDelegate in your UITableViewCell and pass the UITextField like the following:

func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
    delegate?.cell(self, willStartEditing: textField)
    return true
}

func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
    delegate?.cell(self, willEndEditing: textField)
    return true
}

Upvotes: 1

Jayden Irwin
Jayden Irwin

Reputation: 987

jqgsninimo's solution updated to 4.2. Works on iOS 12.0.

Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow(with:)), name: UIResponder.keyboardDidShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(with:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardDidShowNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}

@objc func keyboardDidShow(with notification: Notification) {
    guard let userInfo = notification.userInfo as? [String: AnyObject],
        let keyboardFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
        else { return }

    var contentInset = self.tableView.contentInset
    contentInset.bottom += keyboardFrame.height

    tableView.contentInset = contentInset
    tableView.scrollIndicatorInsets = contentInset
}

@objc func keyboardWillHide(with notification: Notification) {
    guard let userInfo = notification.userInfo as? [String: AnyObject],
        let keyboardFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
        else { return }

    var contentInset = self.tableView.contentInset
    contentInset.bottom -= keyboardFrame.height

    tableView.contentInset = contentInset
    tableView.scrollIndicatorInsets = contentInset
}

Upvotes: 2

Joaquin Pereira
Joaquin Pereira

Reputation: 2251

I did a mix between the answer of @Victor and some code that I had. I created a handy extension that can be use across many UITextField.

1.- First we need an extension for the UITextField to manage the keyboard notifications

extension UITextField {

    func keepTextFieldAboveKeyboard(tableView:UITableView) {

        var willShowNotification: NSObjectProtocol?
        var willHideNotification: NSObjectProtocol?

        willShowNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillShow, object: nil, queue: OperationQueue.main) {(notification) in

            var userInfo = notification.userInfo!

            if let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
                // Get my height size
                let myheight = tableView.frame.height
                // Get the top Y point where the keyboard will finish on the view
                let keyboardEndPoint = myheight - keyboardFrame.height
                // Get the the bottom Y point of the textInput and transform it to the currentView coordinates.
                if let pointInTable = self.superview?.convert(self.frame.origin, to: tableView) {

                    let textFieldBottomPoint = pointInTable.y + self.frame.size.height + 20

                    // Finally check if the keyboard will cover the textInput
                    if keyboardEndPoint <= textFieldBottomPoint {

                        tableView.contentOffset.y = textFieldBottomPoint - keyboardEndPoint
                    } else {

                        tableView.contentOffset.y = 0
                    }
                }
            }

            NotificationCenter.default.removeObserver(willShowNotification!)
        }

        willHideNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillHide, object: nil, queue: OperationQueue.main) { (notification) in

            tableView.contentOffset.y = 0

            NotificationCenter.default.removeObserver(willHideNotification!)
        }

    }

}

2.- This previous extension should be call inside textFieldShouldBeginEditing so we can have an extension for that as well

extension UITextField : UITextFieldDelegate {

    public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool  {

        guard let tableView = textField.parentView(of: UITableView.self) else { return true };

        textField.keepTextFieldAboveKeyboard(tableView:tableView);

        return true;

    }
}

3.- Finally as you notice in the previous method, we need the TableView where the cell with the UITextField is located. So we can use this extension (Which is also useful for other purposes)

extension UIView {
    func parentView<T: UIView>(of type: T.Type) -> T? {
        guard let view = self.superview else {
            return nil
        }
        return (view as? T) ?? view.parentView(of: T.self)
    }
}

4.- Finally in the cell where the UITextField is located, we just write this simple line of code

textField.delegate = textField;

Completely reusable and universal.

Upvotes: 3

Susim Samanta
Susim Samanta

Reputation: 1593

In swift4 we can create an extension of UIViewController.

extension UIViewController {
    //MARK: Keyboard user interactions
    func handleContainerViewFrameOnKeyboardShowHide(originalContainerFrame: CGRect) {
        NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillShow, object: nil, queue: OperationQueue.main) {(notification) in
            var info = (notification as NSNotification).userInfo!
            let keyboardFrame: CGRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
            var tableViewFrame = originalContainerFrame
            tableViewFrame.size.height = originalContainerFrame.size.height - keyboardFrame.size.height
            self.view.frame = tableViewFrame
        }
        NotificationCenter.default.addObserver(forName: NSNotification.Name.UIKeyboardWillHide, object: nil, queue: OperationQueue.main) { (notification) in
            self.view.frame = originalContainerFrame
        }
    }
}

Upvotes: 0

jqgsninimo
jqgsninimo

Reputation: 7068

My solution (worked on Xcode 8.3.3, Swift 3, iOS 10.3.1):

class MyTableViewController: UITableViewController {

    override func viewWillAppear(_ animated: Bool) {
        NotificationCenter.default.addObserver(self, selector: #selector(actionKeyboardDidShow(with:)), name: .UIKeyboardDidShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(actionKeyboardWillHide(with:)), name: .UIKeyboardWillHide, object: nil)
    }

    override func viewDidDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: .UIKeyboardDidShow, object: nil)
        NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
    }

    @objc private func actionKeyboardDidShow(with notification: Notification) {
        guard let userInfo = notification.userInfo as? [String: AnyObject],
            let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
            else { return }

        var contentInset = self.tableView.contentInset
        contentInset.bottom += keyboardFrame.height

        self.tableView.contentInset = contentInset
        self.tableView.scrollIndicatorInsets = contentInset
    }

    @objc private func actionKeyboardWillHide(with notification: Notification) {
        guard let userInfo = notification.userInfo as? [String: AnyObject],
            let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
            else { return }

        var contentInset = self.tableView.contentInset
        contentInset.bottom -= keyboardFrame.height

        self.tableView.contentInset = contentInset
        self.tableView.scrollIndicatorInsets = contentInset
    }

}

Upvotes: 5

Georgios Iniatis
Georgios Iniatis

Reputation: 71

This answer is for Xamarin iOS users, however someone can easily modify it to make it work with Swift or Objective C.

The solution is very simple and has worked perfectly in my case where I needed a cell to appear on the top half of the screen in order for the cell's textfield to be above keyboard. So what I did is the following:

I implemented textField.ShouldBeginEditing the following way:

myTextField.ShouldBeginEditing = textField =>
{
   myTableView.ScrollToRow(indexPath, UITableViewScrollPosition.Top, true);

   return true;
};

Where:

myTextField is the cell's text field,

myTableView is the table view,

indexPath is the cell's index path within the table view.

As I previously mentioned, this solution works perfectly in my case so I thought I could share it with you guys in case it works for you as well. Happy coding :)

Upvotes: 2

chiri
chiri

Reputation: 163

I made a mix with the answers of Matt and also Salman. This works for many textfields in a tableView. Swift 3

Basically is get the BOTTOM Y point of the textInput touched. Once we have that I check if the keyboard cover the textInput and if do it I would change the contentOffset of the tableView

First register to the notifications. In the init if is a UIView or in viewDidLoad if is a UIViewController:

    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

Then set up the textfield touched as current input

var inputActive: UITextField!

func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    inputActive = textInput
    return true
}

Finally implement the notifications method:

func keyboardWillShow(notification: NSNotification) {
    var userInfo = notification.userInfo!
    if let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
        // Get my height size
        let myheight = tableView.frame.height
        // Get the top Y point where the keyboard will finish on the view
        let keyboardEndPoint = myheight - keyboardFrame.height
        // Get the the bottom Y point of the textInput and transform it to the currentView coordinates.
        if let pointInTable = inputActive.superview?.convert(inputActive.frame.origin, to: tableView) {
            let textFieldBottomPoint = pointInTable.y + inputActive.frame.size.height + 20
            // Finally check if the keyboard will cover the textInput
            if keyboardEndPoint <= textFieldBottomPoint {
                tableView.contentOffset.y = textFieldBottomPoint - keyboardEndPoint
            } else {
                tableView.contentOffset.y = 0
            }
        }
    }
}

func keyboardWillHide(notification: NSNotification) {
    tableView.contentOffset.y = 0
}

Few things to add. The padding is some extra distance that I add in my case was 20 points. Also in my case the UITableView was added to a UIViewController but this should work also in a UITableViewController

Hope this helps!

Upvotes: 11

Parth Changela
Parth Changela

Reputation: 955

Scroll UITextField above Keyboard in a UITableViewCell on a regular UIViewController swift 3 and Xcode 8.1

1) set Delegate = UITextFieldDelegate

  override func viewWillAppear(_ animated: Bool) {

        NotificationCenter.default.addObserver(self, selector: #selector(DayViewController.keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil);
        NotificationCenter.default.addObserver(self, selector: #selector(DayViewController.keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil);

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


//MARK: - Keyboard Show and Hide Methods
    func keyboardWillShow(notification: NSNotification)
    {
        let info = notification.userInfo! as! [String: AnyObject],
        kbSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue.size,
        contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height, right: 0)

        self.FourthTblMainTableView.contentInset = contentInsets
        self.FourthTblMainTableView.scrollIndicatorInsets = contentInsets

        var aRect = self.FourthTblMainTableView.frame
        aRect.size.height -= kbSize.height
    }
    func keyboardWillHide(notification: NSNotification)
    {
         let contentInsets = UIEdgeInsets.zero
        self.FourthTblMainTableView.contentInset = contentInsets
        self.FourthTblMainTableView.scrollIndicatorInsets = contentInsets
    }

Upvotes: 1

Salman Hasrat Khan
Salman Hasrat Khan

Reputation: 2006

I tried the link that @inturbidus posted for iOS8 but unfortunately it did not work for me. After a bit of digging, it turns out that Apple does have sample code on their website to make this work naturally as they do in the UITableViewController. Here's the Apple link for the Objective-C version and I'm also adding the swift version here.

Objective-C link from Apple (Scroll to listing 5-1): https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html

Swift adaptation of the Objective-C Version

var activeField: UITextField?

func textFieldDidBeginEditing(textField: UITextField) {
    self.activeField = textField
}

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

func registerForKeyboardNotifications() {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWasShown:", name: UIKeyboardDidShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}    

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)

    self.tableView.contentInset = contentInsets
    self.tableView.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 !CGRectContainsPoint(aRect, activeField!.frame.origin) {
        self.tableView.scrollRectToVisible(activeField!.frame, animated: true)
    }
}

func keyboardWillBeHidden(aNotification: NSNotification) {
    let contentInsets = UIEdgeInsetsZero
    self.tableView.contentInset = contentInsets
    self.tableView.scrollIndicatorInsets = contentInsets
}

Upvotes: 25

Nhan Lam Dai
Nhan Lam Dai

Reputation: 81

Thank @Salman for the link.
Just notice that Apple example is used for scrollview in normal view.
In table view you must convert Point and Rect of activeField to table's coordinate, so I change some lines in function keyboardWasShown:

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)

        self.tableBasket.contentInset = contentInsets
        self.tableBasket.scrollIndicatorInsets = contentInsets

        var aRect = self.view.frame
        aRect.size.height -= kbSize.height

        let pointInTable = activeField!.superview!.convertPoint(activeField!.frame.origin, toView: tableView)
        let rectInTable = activeField!.superview!.convertRect(activeField!.frame, toView: tableView)

        if !CGRectContainsPoint(aRect, pointInTable) {
            self.tableView.scrollRectToVisible(rectInTable, animated: true)
        }
}

And if your view has tabBarController, remember to reset contentInsets with tabBar height instead of UIEdgeInsetsZero (if not, your last bottom rows maybe hiden under tabBar):

func keyboardWillBeHidden(aNotification: NSNotification) {
        //UIEdgeInsetsZero is used in view without tabBar
        //let contentInsets = UIEdgeInsetsZero
        let tabBarHeight = self.tabBarController!.tabBar.frame.height
        let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: tabBarHeight, right: 0)
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    }
}

Upvotes: 8

Matt Hudson
Matt Hudson

Reputation: 7358

I spent all day trying to figure this out. I posted it here, then found a blog link and an incredibly simple solution. It looks like this:

-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    CGPoint pointInTable = [textField.superview convertPoint:textField.frame.origin toView:self.tableView];
    CGPoint contentOffset = self.tableView.contentOffset;

    contentOffset.y = (pointInTable.y - textField.inputAccessoryView.frame.size.height);

    NSLog(@"contentOffset is: %@", NSStringFromCGPoint(contentOffset));

    [self.tableView setContentOffset:contentOffset animated:YES];

    return YES;
}


-(BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
    [textField resignFirstResponder];

    if ([textField.superview.superview isKindOfClass:[UITableViewCell class]])
    {
        UITableViewCell *cell = (UITableViewCell*)textField.superview.superview;
        NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];

        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:TRUE];
    }

    return YES;
}

Check this for iOS 8

Upvotes: 53

Related Questions