让UITableView滚动到选定的UITextField,并避免被键盘遮挡。

46

我在一个UIViewController的表视图中有一个UITextField(不是UITableViewController)。如果表视图在UITableViewController上,表格将自动滚动到正在编辑的textField,以防止其被键盘遮挡。但是在UIViewController上却没有这样的效果。

我尝试了几天,阅读了多种方式来尝试实现此目标,但我无法让它工作。最接近实际滚动的方法是:

-(void) textFieldDidBeginEditing:(UITextField *)textField {

// SUPPOSEDLY Scroll to the current text field

CGRect textFieldRect = [textField frame];
[self.wordsTableView scrollRectToVisible:textFieldRect animated:YES];

}

然而,这只能将表格滚动到最顶端的行。

看起来简单的任务已经让我沮丧了几天。

我正在使用以下代码来构建tableView的单元格:

- (UITableViewCell *)tableView:(UITableView *)aTableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {

NSString *identifier = [NSString stringWithFormat: @"%d:%d", [indexPath indexAtPosition: 0], [indexPath indexAtPosition:1]];

UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil) {

        cell = [[[UITableViewCell alloc] 
        initWithStyle:UITableViewCellStyleDefault 
        reuseIdentifier:identifier] autorelease];

        cell.accessoryType = UITableViewCellAccessoryNone;

        UITextField *theTextField = [[UITextField alloc] initWithFrame:CGRectMake(180, 10, 130, 25)];

        theTextField.adjustsFontSizeToFitWidth = YES;
        theTextField.textColor = [UIColor redColor];
        theTextField.text = [textFieldArray objectAtIndex:indexPath.row];
        theTextField.keyboardType = UIKeyboardTypeDefault;
        theTextField.returnKeyType = UIReturnKeyDone;
        theTextField.font = [UIFont boldSystemFontOfSize:14];
        theTextField.backgroundColor = [UIColor whiteColor];
        theTextField.autocorrectionType = UITextAutocorrectionTypeNo;
        theTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
        theTextField.clearsOnBeginEditing = NO;
        theTextField.textAlignment = UITextAlignmentLeft;

        //theTextField.tag = 0;
        theTextField.tag=indexPath.row;

        theTextField.delegate = self;

        theTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
        [theTextField setEnabled: YES];

        [cell addSubview:theTextField];

        [theTextField release];


}

return cell;
}
我觉得如果我能在textFieldDidBeginEditing方法中以某种方式传递indexPath.row,就可以使tableView正确滚动?感激不尽任何帮助。
13个回答

109

在我的应用中,我已经成功地使用了contentInsetscrollToRowAtIndexPath的组合,代码如下:

当你想要显示键盘时,只需向底部添加一个与所需高度相同的表格contentInset

tableView.contentInset =  UIEdgeInsetsMake(0, 0, height, 0);

然后,您可以安全使用

[tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:cell_index inSection:cell_section] animated:YES];

通过添加contentInset,即使您正在关注最后一个单元格,tableView仍然可以滚动。只需确保在关闭键盘时重置contentInset。

编辑:
如果您只有一个部分(可以用0替换cell_section ),并使用textView标记来通知单元格行。


你的意思是:tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, height); - Lauren Quantrell
抱歉,我的错。我是凭记忆打的这个内容。我已经编辑了我的帖子来纠正这个错误。 - Andrei Stanescu
好消息是现在当我使用以下代码时:[[wordsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:5 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES]; 我确实让tableView滚动到第5行。但我需要找出正在调用哪一行。 - Lauren Quantrell
3
我认为你的意思是UIEdgeInsetsMake(0, 0, height, 0),除此之外,这是一个非常优雅的解决方案!赞! - Keller
真不错...这个东西让我疯了。 - Jeremy Piednoel
显示剩余3条评论

49

Swift

@objc private func keyboardWillShow(_ notification: Notification) {
    guard let userinfo = notification.userInfo else {
        return
    }

    guard
        let duration = (userinfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue,
        let endFrame = (userinfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
        let curveOption = userinfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
            return
    }
    
    UIView.animate(withDuration: duration, delay: 0, options: [.beginFromCurrentState, .init(rawValue: curveOption)], animations: {
        let edgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: endFrame.height, right: 0)
        self.scrollView.contentInset = edgeInsets
        self.scrollView.scrollIndicatorInsets = edgeInsets
    })
}

@objc private func keyboardWillHide(_ notification: Notification) {
    guard let userinfo = notification.userInfo else {
        return
    }

    guard
        let duration = (userinfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue,
        let curveOption = userinfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
            return
    }
    
    UIView.animate(withDuration: duration, delay: 0, options: [.beginFromCurrentState, .init(rawValue: curveOption)], animations: {
        let edgeInsets = UIEdgeInsets.zero
        self.scrollView.contentInset = edgeInsets
        self.scrollView.scrollIndicatorInsets = edgeInsets
    })
}


override func viewDidLoad() {
    super.viewDidLoad()
    
    // ...

    subscribeToKeyboardNotifications()
}

deinit {
    unsubscribeFromKeyboardNotifications()
}

private func subscribeToKeyboardNotifications() {
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIWindow.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIWindow.keyboardWillHideNotification, object: nil)
}

private func unsubscribeFromKeyboardNotifications() {
    NotificationCenter.default.removeObserver(self, name: UIWindow.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: UIWindow.keyboardWillHideNotification, object: nil)
}

Objective C

- (void)keyboardWillShow:(NSNotification *)sender
{
    CGFloat height = [[sender.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
    NSTimeInterval duration = [[sender.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    UIViewAnimationOptions curveOption = [[sender.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue] << 16;
    
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState|curveOption animations:^{
        UIEdgeInsets edgeInsets = UIEdgeInsetsMake(0, 0, height, 0);
        tableView.contentInset = edgeInsets;
        tableView.scrollIndicatorInsets = edgeInsets;
    } completion:nil];
}

- (void)keyboardWillHide:(NSNotification *)sender
{
    NSTimeInterval duration = [[sender.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    UIViewAnimationOptions curveOption = [[sender.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue] << 16;

    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState|curveOption animations:^{
        UIEdgeInsets edgeInsets = UIEdgeInsetsZero;
        tableView.contentInset = edgeInsets;
        tableView.scrollIndicatorInsets = edgeInsets;
    } completion:nil];
}

在 - (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)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(IBAction)是用于需要链接到Interface Builder组件的方法。请使用-(void)。您没有指定此处如何获取indexPath。 - quantumpotato
@quantumpotato 是的,正确的。-(void) textFieldDidBeginEditing:(UITextField *)textField { UITableViewCell *cell = (UITableViewCell *)[textField superview]; NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; } - FunkyKat
但是这种方法在iOS6上不起作用。在iOS6上有任何替代方法吗? - dineshsurya
同意@dineshsurya的观点,有没有修复方法可以让它在iOS6上运行? - opfeffer
谢谢您,这是我能找到的最佳答案。 - Batnom
解决方案很好用,但发现它抬高了 44(文本字段的高度)。所以将这段代码替换为键盘会显示:UIEdgeInsets edgeInsets = UIEdgeInsetsMake(0, 0, kbSize.height-44, 0); 然后就可以正常工作了。如果我有误或漏掉了什么,请指正。谢谢。 - Yogesh Lolusare

10
这是对FunkyKat答案的微调(非常感谢FunkyKat!)。为了适应未来的iOS兼容性,最好不要硬编码UIEdgeInsetsZero。
相反,我会请求当前插入值,并根据需要微调底部值。
- (void)keyboardWillShow:(NSNotification *)sender {
    CGSize kbSize = [[[sender userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    NSTimeInterval duration = [[[sender userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

    CGFloat height = UIDeviceOrientationIsPortrait([[UIDevice currentDevice] orientation]) ? kbSize.height : kbSize.width;
    if (isIOS8()) height = kbSize.height;

    [UIView animateWithDuration:duration animations:^{
        UIEdgeInsets edgeInsets = [[self tableView] contentInset];
        edgeInsets.bottom = height;
        [[self tableView] setContentInset:edgeInsets];
        edgeInsets = [[self tableView] scrollIndicatorInsets];
        edgeInsets.bottom = height;
        [[self tableView] setScrollIndicatorInsets:edgeInsets];
    }];
}

- (void)keyboardWillHide:(NSNotification *)sender {
    NSTimeInterval duration = [[[sender userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

    [UIView animateWithDuration:duration animations:^{
        UIEdgeInsets edgeInsets = [[self tableView] contentInset];
        edgeInsets.bottom = 0;
        [[self tableView] setContentInset:edgeInsets];
        edgeInsets = [[self tableView] scrollIndicatorInsets];
        edgeInsets.bottom = 0;
        [[self tableView] setScrollIndicatorInsets:edgeInsets];
    }];
}

如何从捕获此类通知中获取indexPath? - fatuhoku
我在注释我的答案,因为昨天有人编辑了我的答案,而我不同意。他们在第五行交换了高度和重量参数。那是错误的。在iOS8之前,我的原始答案是正确的。在iOS8之后,苹果公司更改了这些高度和宽度值以根据方向返回(或者是相反的?),因此需要使用 if isIOS8() 调用。我将让读者自己实现该方法。 - bmauter

8

为了帮助其他遇到此问题的人,我在这里发布必要的方法:

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *identifier = [NSString stringWithFormat: @"%d:%d", [indexPath indexAtPosition: 0], [indexPath indexAtPosition:1]];

    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil) {

        cell = [[[UITableViewCell alloc]  initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];

        UITextField *theTextField = [[UITextField alloc] initWithFrame:CGRectMake(180, 10, 130, 25)];

        theTextField.keyboardType = UIKeyboardTypeDefault;
        theTextField.returnKeyType = UIReturnKeyDone;
        theTextField.clearsOnBeginEditing = NO;
        theTextField.textAlignment = UITextAlignmentLeft;

        // (The tag by indexPath.row is the critical part to identifying the appropriate
        // row in textFieldDidBeginEditing and textFieldShouldEndEditing below:)

        theTextField.tag=indexPath.row;

        theTextField.delegate = self;

        theTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
        [theTextField setEnabled: YES];

        [cell addSubview:theTextField];

        [theTextField release];

    }

    return cell;
}

-(void) textFieldDidBeginEditing:(UITextField *)textField {

    int z = textField.tag;                                              

    if (z > 4) {

        // Only deal with the table row if the row index is 5 
        // or greater since the first five rows are already 
        // visible above the keyboard   

        // resize the UITableView to fit above the keyboard

        self.wordsTableView.frame = CGRectMake(0.0,44.0,320.0,200.0);       

        // adjust the contentInset

        wordsTableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 10);        

        // Scroll to the current text field

        [wordsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:z inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];

    }
}


- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {

    // Determine which row is being edited

    int z = textField.tag;  

    if (z > 4) {

        // resize the UITableView to the original size

        self.wordsTableView.frame = CGRectMake(0.0,44.0,320.0,416.0);       

        // Undo the contentInset
        wordsTableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);         

    }

    return YES;

}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

    // Dismisses the keyboard when the "Done" button is clicked

    [textField resignFirstResponder];

    return YES;                                 

}

3

我需要一个简单的解决方案,对我来说 这篇文章很有帮助:

func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        let pointInTable = textField.superview!.convert(textField.frame.origin, to: tableView)
        var tableVContentOffset = tableView.contentOffset
        tableVContentOffset.y = pointInTable.y
        if let accessoryView = textField.inputAccessoryView {
            tableVContentOffset.y -= accessoryView.frame.size.height
        }
        tableView.setContentOffset(tableVContentOffset, animated: true)
        return true;
    }

enter image description here


你可以把它变得更小,不需要调用superview,你可以创建一个cgpoint变量,其中包含contentOffset.x和point.y,然后直接将其设置为tableview的contentOffset。 - thibaut noah

1
尝试我的编程,这会对你有所帮助。
tabelview.contentInset =  UIEdgeInsetsMake(0, 0, 210, 0);
[tableview scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:your_indexnumber inSection:Your_section]
                 atScrollPosition:UITableViewScrollPositionMiddle animated:NO];

1

苹果官方发布了一篇帖子,解释了如何像在UITableViewController中那样自然地完成此操作。我的Stackoverflow答案中也有这个解释以及一个Swift版本。

https://dev59.com/AGUp5IYBdhLWcg3wzp5k#31869898


0

你需要调整tableView的大小,以便它不会被键盘遮挡。

-(void) textFieldDidBeginEditing:(UITextField *)textField {

// SUPPOSEDLY Scroll to the current text field
self.worldsTableView.frame = CGRectMake(//make the tableView smaller; to only be in the area above the keyboard);
CGRect textFieldRect = [textField frame];
[self.wordsTableView scrollRectToVisible:textFieldRect animated:YES];

}

另外,您可以使用键盘通知;这样做的效果会稍微好一些,因为您将获得更多的信息,并且在了解键盘何时弹出方面更加一致:

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

然后实现:

- (void)keyboardWillShow:(NSNotification *)notification {

}
- (void)keyboardWillHide:(NSNotification *)notification {

}

我将表格大小调整为200像素以适应键盘上方,但以下代码只会将表格向上滚动到第一行,而不是向下滚动到隐藏的行:CGRect textFieldRect = [textField frame]; [self.wordsTableView scrollRectToVisible:textFieldRect animated:YES]; - Lauren Quantrell
你可以用 scrollToRowAtIndexPath: 替代吗?这会更简单。如果不行,问题在于 textFieldRect 是 textField 的 frame,在它的 superview (也就是 contentView 或 tableViewCell)中的 frame。你需要将该矩形转换到 tableView 的坐标系中,而不是 tableViewCell 中的坐标系。使用 convertRect:toView: 即可完成此操作。 - GendoIkari
我发现textFieldRect只给了我第一行,因为它是一个视图中的框架,正如你所建议的那样,这就是为什么它只滚动到最顶部行。我不知道如何使用convertRect:toView:方法。 - Lauren Quantrell
CGRect myFrame = [textField convertRect:textFieldRect toView:self.worldsTableView]; 然后滚动到myFrame而不是textFieldRect。 - GendoIkari
1
CGRect myFrame = [textField convertRect:textFieldRect toView:self.worldsTableView]; 也不起作用... - Lauren Quantrell

0

我的代码。也许对某些人有用:

自定义tableView中的textField单元格

.m

    @property (nonatomic, strong) UITextField *currentCellTextField;

       CustomCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier2];
            if (cell == nil) {
                NSArray * nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
                cell = (CustomCell *)[nib objectAtIndex:0];
                cell.textfield.delegate = self;
            }
      - (void) textFieldDidBeginEditing:(UITextField *)textField
 {
  self.currentCellTextField = textField;

  CGPoint pnt = [self.organisationTableView convertPoint:textField.bounds.origin fromView:textField];
  NSIndexPath* path = [self.organisationTableView indexPathForRowAtPoint:pnt];

if (path.section >= 2) {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.3];
    self.organisationTableView.contentInset = UIEdgeInsetsMake(0, 0, kOFFSET_FOR_KEYBOARD, 0);
    CGPoint siize = self.organisationTableView.contentOffset;
    siize.y =(pnt.y-170);
    self.organisationTableView.contentOffset = CGPointMake(0, siize.y);
    [UIView commitAnimations];
    }
 }

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

CGPoint pnt = [self.organisationTableView convertPoint:textField.bounds.origin fromView:textField];
NSIndexPath* path = [self.organisationTableView indexPathForRowAtPoint:pnt];

 if (path.section >= 2) {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.3];
    self.organisationTableView.contentInset = UIEdgeInsetsZero;
    self.organisationTableView.contentOffset = CGPointMake(0, self.organisationTableView.contentOffset.y);
    [UIView commitAnimations];
      }

 return YES;
   }

0
你可以尝试将UITableViewController添加到UIViewController中,而不仅仅是一个表视图。这样一来,你就可以调用UITableViewController的viewWillAppear方法,一切都会正常显示。
示例:
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [tableViewController viewWillAppear:animated];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接