如何以编程方式移动 UIScrollView 以集中在键盘上方的控件中?

Posted

技术标签:

【中文标题】如何以编程方式移动 UIScrollView 以集中在键盘上方的控件中?【英文标题】:How programmatically move a UIScrollView to focus in a control above keyboard? 【发布时间】:2009-01-27 19:27:45 【问题描述】:

我的UIScrollView 上有 6 个UITextFields。现在,我可以按用户请求滚动。但是当键盘出现时,一些文本字段被隐藏了。

这不是用户友好的。

如何以编程方式滚动视图以确保键盘不会隐藏文本字段?

【问题讨论】:

查看我为***.com/questions/484839/…发布的答案 【参考方案1】:

这对我有用。 在为键盘调整视图之前有一个实例变量保存 UIScrollView 的偏移值,因此您可以在 UITextField 返回后恢复之前的状态:

//header
@interface TheViewController : UIViewController <UITextFieldDelegate> 
    CGPoint svos;



//implementation
- (void)textFieldDidBeginEditing:(UITextField *)textField 
    svos = scrollView.contentOffset;
    CGPoint pt;
    CGRect rc = [textField bounds];
    rc = [textField convertRect:rc toView:scrollView];
    pt = rc.origin;
    pt.x = 0;
    pt.y -= 60;
    [scrollView setContentOffset:pt animated:YES];           


- (BOOL)textFieldShouldReturn:(UITextField *)textField 
    [scrollView setContentOffset:svos animated:YES]; 
    [textField resignFirstResponder];
    return YES;

【讨论】:

只是一个小补充:把 svos = scrollView.contentOffset;如果您有多个 UITextFields,请在 ViewDidLoad 方法中。因为如果您从一个文本字段转到下一个文本字段而不隐藏键盘 svos 将被覆盖,因此恢复内容偏移量将失败。 在我的情况下,我不想恢复视图首次加载时存在的偏移量,而是在访问每个单独的 UITextField 之前。 可以给我解释一下 CGPoint pt; CGRect rc = [文本字段边界]; rc = [textField convertRect:rc toView:scrollView]; pt = rc.origin; pt.x = 0; pt.y -= 60;这是什么意思。我正在寻找的是在 ScrollView 中有很多 TextField 从 xcode 4.3.1 开始,此代码对我不起作用 - textFieldDidBeginEditing 接收 NSNotification 对象,而不是 UITextField ...除非我遗漏了什么。我能够解决这个问题,但不是以与这个问题相关的方式。 @Dakine83 您缺少一些东西:- (void)textFieldDidBeginEditing:(UITextField *)textField 在文档中。你的 Xcode 版本也不重要,重要的是 ios 版本。【参考方案2】:

最后,一个简单的修复:

UIScrollView* v = (UIScrollView*) self.view ;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:v];
rc.origin.x = 0 ;
rc.origin.y -= 60 ;

rc.size.height = 400;
[self.scroll scrollRectToVisible:rc animated:YES];

现在我认为只是将它与上面的链接结合起来就可以了!

【讨论】:

适用于 iOS8。我不知道底部的 TextFields,但一种解决方法可能是将可滚动内容向下扩展虚拟键盘的高度。您将在底部有空白空间,但在编辑时该空间将被虚拟键盘占用。【参考方案3】:

我将一个通用的、插入式 UIScrollView 和 UITableView 子类放在一起,负责将其中的所有文本字段移到键盘之外。

当键盘即将出现时,子类将找到即将被编辑的子视图,并调整其框架和内容偏移量以确保该视图可见,并带有与键盘弹出框相匹配的动画。当键盘消失时,它会恢复到原来的大小。

它应该适用于基本上任何设置,无论是基于 UITableView 的界面,还是由手动放置的视图组成的界面。

Here 是。


(For google: TPKeyboardAvoiding, TPKeyboardAvoidingScrollView, TPKeyboardAvoidingCollectionView.)
Editor's note: TPKeyboardAvoiding seems to be continually updated and fresh, as of 2014.

【讨论】:

我一直在努力解决这个问题中的错误。首先,在我找到关于设置 self.myScrollView.contentInset = UIEdgeInsetsMake(0, 0, self.view.bounds.size.height + 432, 0); 的提示之前不会发生滚动。 //(上、左、下、右) 现在,即使该字段在我关注它之前滚动到键盘所在的位置上方,滚动也会撤消并且键盘会覆盖它。这非常令人沮丧。我将使用codeproject.com/Articles/744418/Virtual-Keyboard-in-iOS-Part 和此页面上的其他答案来做这件事。【参考方案4】:

如果您将文本字段的delegate 设置为程序中的控制器对象,则可以让该对象实现textFieldDidBeginEditing:textFieldShouldReturn: 方法。然后可以使用第一种方法滚动到您的文本字段,然后可以使用第二种方法向后滚动。

您可以在我的博客中找到我为此使用的代码:Sliding UITextViews around to avoid the keyboard。我没有针对 UIScrollView 中的文本视图测试此代码,但它应该可以工作。

【讨论】:

【参考方案5】:

简单而最好的

- (void)textFieldDidBeginEditing:(UITextField *)textField


  // self.scrlViewUI.contentOffset = CGPointMake(0, textField.frame.origin.y);
    [_scrlViewUI setContentOffset:CGPointMake(0,textField.center.y-90) animated:YES];
    tes=YES;
    [self viewDidLayoutSubviews];

【讨论】:

这个实现没有错。工作得很好。【参考方案6】:

到目前为止发布的答案对我不起作用,因为我的 UIViews 嵌套结构非常深。此外,我遇到的问题是其中一些答案仅适用于某些设备方向。

这是我的解决方案,希望能让您在这方面浪费更少的时间。

我的 UIViewTextView 派生自 UIView,是一个 UITextView 委托,并在从该 UITextView 的 XML 文件中读取了一些参数后添加了一个 UITextView(为清楚起见,此处省略了该 XML 部分)。

这是私有接口定义:

#import "UIViewTextView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreGraphics/CGColor.h>

@interface UIViewTextView (/**/) 
  @private
  UITextView *tf;

  /*
   * Current content scroll view
   * position and frame
   */
  CGFloat currentScrollViewPosition;
  CGFloat currentScrollViewHeight;
  CGFloat kbHeight;
  CGFloat kbTop;

  /*
   * contentScrollView is the UIScrollView
   * that contains ourselves.
   */
  UIScrollView contentScrollView;

@end

在 init 方法中我必须注册事件处理程序:

@implementation UIViewTextView

- (id) initWithScrollView:(UIScrollView*)scrollView 
  self              = [super init];

  if (self) 
    contentScrollView = scrollView;

    // ...

    tf = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 241, 31)];

    // ... configure tf and fetch data for it ...

    tf.delegate = self;

    // ...

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(keyboardWasShown:) name: UIKeyboardWillShowNotification object:nil];
    [nc addObserver:self selector:@selector(keyboardWasHidden:) name: UIKeyboardWillHideNotification object:nil];
    [self addSubview:tf];
  

  return(self);

完成后,我们需要处理键盘显示事件。这在调用 textViewBeginEditing 之前被调用,因此我们可以使用它来找出键盘的一些属性。本质上,我们想知道键盘的高度。不幸的是,这需要从横向模式下的宽度属性中获取:

-(void)keyboardWasShown:(NSNotification*)aNotification 
  NSDictionary* info                 = [aNotification userInfo];
  CGRect kbRect                      = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
  CGSize kbSize                      = kbRect.size;

  CGRect screenRect                  = [[UIScreen mainScreen] bounds];
  CGFloat sWidth                     = screenRect.size.width;
  CGFloat sHeight                    = screenRect.size.height;

  UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];

  if ((orientation == UIDeviceOrientationPortrait)
      ||(orientation == UIDeviceOrientationPortraitUpsideDown)) 
    kbHeight     = kbSize.height;
    kbTop        = sHeight - kbHeight;
   else 
    //Note that the keyboard size is not oriented
    //so use width property instead
    kbHeight     = kbSize.width;
    kbTop        = sWidth - kbHeight;
  

接下来,我们需要在开始编辑时实际滚动。我们在这里这样做:

- (void) textViewDidBeginEditing:(UITextView *)textView 
  /*
   * Memorize the current scroll position
   */
  currentScrollViewPosition = contentScrollView.contentOffset.y;

  /*
   * Memorize the current scroll view height
   */
  currentScrollViewHeight   = contentScrollView.frame.size.height;

  // My top position
  CGFloat myTop    = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow.rootViewController.view].y;

  // My height
  CGFloat myHeight = self.frame.size.height;

  // My bottom
  CGFloat myBottom = myTop + myHeight;

  // Eventual overlap
  CGFloat overlap  = myBottom - kbTop;

  /*
   * If there's no overlap, there's nothing to do.
   */
  if (overlap < 0) 
    return;
  

  /*
   * Calculate the new height
   */
  CGRect crect = contentScrollView.frame;
  CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight + overlap);

  /*
   * Set the new height
   */
  [contentScrollView setFrame:nrect];

  /*
   * Set the new scroll position
   */
  CGPoint npos;

  npos.x = contentScrollView.contentOffset.x;
  npos.y = contentScrollView.contentOffset.y + overlap;

  [contentScrollView setContentOffset:npos animated:NO];

当我们结束编辑时,我们这样做是为了重置滚动位置:

- (void) textViewDidEndEditing:(UITextView *)textView 
  /*
   * Reset the scroll view position
   */
  CGRect crect = contentScrollView.frame;
  CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight);

  [contentScrollView setFrame:nrect];

  /*
   * Reset the scroll view height
   */
  CGPoint npos;

  npos.x = contentScrollView.contentOffset.x;
  npos.y = currentScrollViewPosition;

  [contentScrollView setContentOffset:npos animated:YES];
  [tf resignFirstResponder];

  // ... do something with your data ...


在键盘中没有什么可做的是隐藏事件处理程序;我们还是把它留在里面:

-(void)keyboardWasHidden:(NSNotification*)aNotification 

就是这样。

/*
   // Only override drawRect: if you perform custom drawing.
   // An empty implementation adversely affects performance during animation.
   - (void)drawRect:(CGRect)rect
   
   // Drawing code
   
 */

@end

【讨论】:

【参考方案7】:

我知道这已经过时了,但上述解决方案仍然没有一个具有“完美”无错误、向后兼容和无闪烁动画所需的所有花哨的定位功能。 分享一下我的解决方案(假设你已经设置了UIKeyboardWill(Show|Hide)Notification):

// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification

    // if we have no view or are not visible in any window, we don't care
    if (!self.isViewLoaded || !self.view.window) 
        return;
    

    NSDictionary *userInfo = [notification userInfo];

    CGRect keyboardFrameInWindow;
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];

    // the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
    CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];

    CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
    UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);

    // this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
    [UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];

    _scrollView.contentInset = newContentInsets;
    _scrollView.scrollIndicatorInsets = newContentInsets;

    /*
     * Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
     * that should be visible, e.g. a purchase button below an amount text field
     * it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
     */
    if (_focusedControl) 
        CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
        controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.

        CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
        CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;

        // this is the visible part of the scroll view that is not hidden by the keyboard
        CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;

        if (controlVisualBottom > scrollViewVisibleHeight)  // check if the keyboard will hide the control in question
            // scroll up until the control is in place
            CGPoint newContentOffset = _scrollView.contentOffset;
            newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);

            // make sure we don't set an impossible offset caused by the "nice visual offset"
            // if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
            newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);

            [_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
         else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) 
            // if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
            CGPoint newContentOffset = _scrollView.contentOffset;
            newContentOffset.y = controlFrameInScrollView.origin.y;

            [_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
        
    

    [UIView commitAnimations];



// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification

    // if we have no view or are not visible in any window, we don't care
    if (!self.isViewLoaded || !self.view.window) 
        return;
    

    NSDictionary *userInfo = notification.userInfo;

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
    [UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];

    // undo all that keyboardWillShow-magic
    // the scroll view will adjust its contentOffset apropriately
    _scrollView.contentInset = UIEdgeInsetsZero;
    _scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;

    [UIView commitAnimations];

【讨论】:

【参考方案8】:

您可以查看:https://github.com/michaeltyson/TPKeyboardAvoiding(我在我的应用程序中使用了该示例)。它工作得很好。希望对你有帮助。


其实,这里有一个使用 TPKeyboardAvoiding 的完整教程,可能对某人有所帮助

(1) 从 github 链接下载 zip 文件。将这 四个 文件添加到您的 Xcode 项目中:

(2) 在 IB 中构建您的美丽表格。添加一个 UIScrollView。 将表单项放在滚动视图内。 (注意 - 关于界面构建器的非常有用的提示:https://***.com/a/16952902/294884)

(3) 点击滚动视图。然后在右上角,第三个按钮,你会看到“UIScrollView”这个词。使用复制和粘贴,将其更改为“TPKeyboardAvoidingScrollView”

(4) 就是这样。将应用放入应用商店,然后向您的客户收费。

(另外,只需单击滚动视图的 Inspector 选项卡。您可能更喜欢打开或关闭弹跳和滚动条 - 您的偏好。)

个人评论 - 我强烈建议在几乎所有情况下对输入表单使用滚动视图(或集合视图)。 不要使用表格视图。这有很多问题。很简单,使用滚动视图非常容易。以任何你想要的方式布置它。它在界面生成器中是 100% 所见即所得。希望对您有所帮助

【讨论】:

当然,这是迈克尔·泰森的课 - 只需向下滚动到下面的答案!太棒了***.com/a/5634493/294884 @JoeBlow 哦,我没看到。我很高兴他回答了。我给了他 +1 票。 对 - 我给了 MT 50 分。这门课非常棒,而且免费,每个人都应该点击它。从字面上看,没有其他解决方案。它调用绝对没有 - 零 - 工作。好吧,我猜你必须在界面生成器中粘贴“TPKeyboardAvoidingScrollView”这个词。令人难以置信的是,有人会费心去做其他事情。它实际上为我的客户在每个项目上节省了 1000 美元。没有其他值得关注的解决方案。【参考方案9】:

这是我的代码,希望对你有帮助。如果您有很多文本字段,它可以正常工作

CGPoint contentOffset;
bool isScroll;
- (void)textFieldDidBeginEditing:(UITextField *)textField 
    contentOffset = self.myScroll.contentOffset;
    CGPoint newOffset;
    newOffset.x = contentOffset.x;
    newOffset.y = contentOffset.y;
    //check push return in keyboar
    if(!isScroll)
        //180 is height of keyboar
        newOffset.y += 180;
        isScroll=YES;
    
   [self.myScroll setContentOffset:newOffset animated:YES];


- (BOOL)textFieldShouldReturn:(UITextField *)textField
    //reset offset of content
    isScroll = NO;
    [self.myScroll setContentOffset:contentOffset animated:YES];
    [textField endEditing:true];
    return  true;

我们有一个点 contentOffset 在键盘显示之前保存滚动视图的 contentoffset。然后我们将滚动 y 的内容约 180(键盘高度)。当您在键盘上触摸返回时,我们会将内容滚动到旧点(它是 contentOffset)。如果您有很多文本字段,则不要在键盘中触摸 return 而是触摸另一个文本字段,它将 +180 。所以我们有检查触摸返回

【讨论】:

我们有一个点 contentOffset 用于在键盘显示之前保存滚动视图的 contentoffset。然后我们将滚动 y 的内容约 180(键盘高度)。当您在键盘上触摸返回时,我们会将内容滚动到旧点(它是 contentOffset)。如果您有很多文本字段,则不要在键盘中触摸 return 而是触摸另一个文本字段,它将 +180 。所以我们有检查触摸返回 请提供一些解释,否则此答案将被删除。【参考方案10】:

使用其中任何一个,

CGPoint bottomOffset = CGPointMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height);

[self.MainScrollView setContentOffset:bottomOffset animated:YES];

[self.MainScrollView scrollRectToVisible:CGRectMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height-30, MainScrollView.frame.size.width, MainScrollView.frame.size.height) animated:YES];

【讨论】:

【参考方案11】:

我认为最好使用键盘通知,因为您不知道第一响应者(焦点所在的控件)是 textField 还是 textView(或其他)。所以只需创建一个类别来查找第一响应者:

#import "UIResponder+FirstResponder.h"

static __weak id currentFirstResponder;

@implementation UIResponder (FirstResponder)

+(id)currentFirstResponder 
   currentFirstResponder = nil;
   [[UIApplication sharedApplication] sendAction:@selector(findFirstResponder:) to:nil from:nil forEvent:nil];
   return currentFirstResponder;


-(void)findFirstResponder:(id)sender 
   currentFirstResponder = self;


@end

然后

-(void)keyboardWillShowNotification:(NSNotification*)aNotification

    contentScrollView.delegate=nil;
    contentScrollView.scrollEnabled=NO;
    contentScrollViewOriginalOffset = contentScrollView.contentOffset;

    UIResponder *lc_firstResponder = [UIResponder currentFirstResponder];
    if([lc_firstResponder isKindOfClass:[UIView class]])

        UIView *lc_view = (UIView *)lc_firstResponder;

        CGRect lc_frame = [lc_view convertRect:lc_view.bounds toView:contentScrollView];
        CGPoint lc_point = CGPointMake(0, lc_frame.origin.y-lc_frame.size.height);
        [contentScrollView setContentOffset:lc_point animated:YES];
    

最终禁用滚动并将委托设置为 nil 然后将其恢复以避免在第一响应者的编辑期间发生一些操作。就像 james_womack 说的,在keyboardWillHideNotification 方法中保留原来的偏移量来恢复它。

-(void)keyboardWillHideNotification:(NSNotification*)aNotification

    contentScrollView.delegate=self;
    contentScrollView.scrollEnabled=YES;
    [contentScrollView setContentOffset:contentScrollViewOriginalOffset animated:YES];

【讨论】:

不知道为什么,但在某些情况下,如果键盘通知是“将”类型,则第一响应者将为零。如果您使用 UIKeyboardDidShowNotification 和 UIKeyboardDidHideNotification 设置通知,则可以避免获得零响应者的机会。【参考方案12】:

在 Swift 1.2+ 中做这样的事情:

class YourViewController: UIViewController, UITextFieldDelegate 

    override func viewDidLoad() 
        super.viewDidLoad()

        _yourTextField.delegate = self //make sure you have the delegate set to this view controller for each of your textFields so textFieldDidBeginEditing can be called for each one
        ...

    

    func textFieldDidBeginEditing(textField: UITextField) 
        var point = textField.convertPoint(textField.frame.origin, toView: _yourScrollView)
        point.x = 0.0 //if your textField does not have an origin at 0 for x and you don't want your scrollView to shift left and right but rather just up and down
        _yourScrollView.setContentOffset(point, animated: true)
    

    func textFieldDidEndEditing(textField: UITextField) 
        //Reset scrollview once done editing
        scrollView.setContentOffset(CGPoint.zero, animated: true)
    


【讨论】:

以上是关于如何以编程方式移动 UIScrollView 以集中在键盘上方的控件中?的主要内容,如果未能解决你的问题,请参考以下文章

如何以编程方式在 UIScrollview 中创建 UIScrollview

以编程方式将触摸传递给 UIScrollView 以进行滚动

以编程方式将 UIScrollView 滚动到 Swift 中的子 UIView(子视图)的顶部

如何以编程方式在UIScrollview中创建UIScrollview

如何以编程方式使用 UIScrollView 使 UIStackView 滚动?

如果我在 UIScrollView 中有 UIViewController,如何以编程方式使 UIScrollview zoomToRect?