【问题标题】:UITextView cursor below frame when changing frame更改框架时,UITextView 光标在框架下方
【发布时间】:2013-08-28 04:59:56
【问题描述】:

我有一个UIViewCOntroller,其中包含一个UITextView。当键盘出现时,我会像这样调整它的大小:

#pragma mark - Responding to keyboard events

- (void)keyboardDidShow:(NSNotification *)notification
{
    NSDictionary* info = [notification userInfo];
    CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGRect newTextViewFrame = self.textView.frame;
    newTextViewFrame.size.height -= keyboardSize.size.height + 70;
    self.textView.frame = newTextViewFrame;
    self.textView.backgroundColor = [UIColor yellowColor];
}

- (void)keyboardWillHide:(NSNotification *)notification
{
    NSDictionary* info = [notification userInfo];
    CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGRect newTextViewFrame = self.textView.frame;
    newTextViewFrame.size.height += keyboardSize.size.height - 70;
    self.textView.frame = newTextViewFrame;
}

textView 似乎调整到正确的大小,但是当用户键入时,光标最终会出现在 textView 框架的“外部”。见下图:

黄色区域是UITextView框(不知道R键旁边的蓝线是什么)。我觉得这很有线。如果这有什么不同,我正在使用 iOS7。

有什么想法或提示吗?

更新

我有一个 UITextView 子类,它使用以下方法绘制水平线(如果有什么不同的话):

- (void)drawRect:(CGRect)rect {

    //Get the current drawing context
    CGContextRef context = UIGraphicsGetCurrentContext();
    //Set the line color and width
    CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:229.0/255.0 green:244.0/255.0 blue:255.0/255.0 alpha:1].CGColor);
    CGContextSetLineWidth(context, 1.0f);
    //Start a new Path
    CGContextBeginPath(context);

    //Find the number of lines in our textView + add a bit more height to draw lines in the empty part of the view
    NSUInteger numberOfLines = (self.contentSize.height + rect.size.height) / self.font.lineHeight;

    CGFloat baselineOffset = 6.0f;

    //iterate over numberOfLines and draw each line
    for (int x = 0; x < numberOfLines; x++) {
        //0.5f offset lines up line with pixel boundary
        CGContextMoveToPoint(context, rect.origin.x, self.font.lineHeight*x + 0.5f + baselineOffset);
        CGContextAddLineToPoint(context, rect.size.width, self.font.lineHeight*x + 0.5f + baselineOffset);
    }

    // Close our Path and Stroke (draw) it
    CGContextClosePath(context);
    CGContextStrokePath(context);
}

【问题讨论】:

  • 我遇到了同样的问题。似乎textView.font.lineHeight 没有考虑调整contentOffset。更正偏移委托的 textViewDidChange: 不起作用,因为它的值会以某种方式恢复。
  • 好的,你是怎么解决的?
  • 这可能只是某个未发布的、NDA 的 SDK 的问题吗?它是否适用于其他 iOS 版本?
  • 这只是一种预感,但您是否尝试过在您的 keyboardDidShow 和 keyboardWillHide 方法中调用 [self.textView setNeedsLayout];
  • 我的第二个想法是:如果您使用的是 AutoLayout,请尝试设置高度约束。将该约束作为 IBOutlet 连接到您的视图控制器。尝试更改自动布局约束,而不是更改框架。请让我知道这是否适合您。

标签: ios objective-c cocoa-touch uitextview uikeyboard


【解决方案1】:

为什么不给你的文本视图一个contentInset(和一个匹配的scrollIndicatorInsets)而不是调整框架的大小?请记住,文本视图实际上是滚动视图。这是处理键盘(或其他)干扰的正确方法。

有关contentInset 的更多信息,请参阅this 问题。


这似乎还不够。仍然使用插入,因为这样更正确(尤其是在 iOS7 上,键盘是透明的),但您还需要对插入符号进行额外处理:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.textView setDelegate:self];
    self.textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
}

- (void)_keyboardWillShowNotification:(NSNotification*)notification
{
    UIEdgeInsets insets = self.textView.contentInset;
    insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
    self.textView.contentInset = insets;

    insets = self.textView.scrollIndicatorInsets;
    insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
    self.textView.scrollIndicatorInsets = insets;
}

- (void)_keyboardWillHideNotification:(NSNotification*)notification
{
    UIEdgeInsets insets = self.textView.contentInset;
    insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
    self.textView.contentInset = insets;

    insets = self.textView.scrollIndicatorInsets;
    insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
    self.textView.scrollIndicatorInsets = insets;
}

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    _oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];

    _caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(_scrollCaretToVisible) userInfo:nil repeats:YES];
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    [_caretVisibilityTimer invalidate];
    _caretVisibilityTimer = nil;
}

- (void)_scrollCaretToVisible
{
    //This is where the cursor is at.
    CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];

    if(CGRectEqualToRect(caretRect, _oldRect))
        return;

    _oldRect = caretRect;

    //This is the visible rect of the textview.
    CGRect visibleRect = self.textView.bounds;
    visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
    visibleRect.origin.y = self.textView.contentOffset.y;

    //We will scroll only if the caret falls outside of the visible rect.
    if(!CGRectContainsRect(visibleRect, caretRect))
    {
        CGPoint newOffset = self.textView.contentOffset;

        newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 5, 0);

        [self.textView setContentOffset:newOffset animated:YES];
    }
}

-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

很多工作,Apple 应该提供更好的处理插入符号的方法,但这是可行的。

【讨论】:

  • 已测试,但我得到了相同的结果。
  • 我将创建一个文本项目并尝试重现您的问题并进行修复。
  • 谢谢,那太好了!
  • 谢谢!很快就会尝试。
  • 这里只需要注意一件事。这是 iOS 7 和 iOS 8 的最佳方法。但您需要记住,每次键盘更改时都会调用 UIKeyboardWillShowNotification。这意味着如果您在 iOS 8 中折叠预测输入面板,这将更改键盘高度并触发具有不同高度的事件。用户还可以在不关闭键盘的情况下拆分键盘。因此,与其对底部插图做 +=,不如真正做一个 '='。这样你就可以省去一些麻烦。隐藏事件也是如此,不要 -= 插入,直接将其设置为 0。
【解决方案2】:

我尝试过的所有其他答案对我来说都有点奇怪。使用NSTimer 执行滚动也意味着用户无法向上滚动,因为插入符号最终会离开屏幕并立即再次向下滚动。最后我还是坚持原来的方法,在键盘通知事件上更改UITextView帧,然后添加了以下方法:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    // Whenever the user enters text, see if we need to scroll to keep the caret on screen
    [self scrollCaretToVisible];
    return YES;
}

- (void)scrollCaretToVisible
{
    //This is where the cursor is at.
    CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];

    // Convert into the correct coordinate system
    caretRect = [self.view convertRect:caretRect fromView:self.textView];

    if(CGRectEqualToRect(caretRect, _oldRect)) {
        // No change
        return;
    }

    _oldRect = caretRect;

    //This is the visible rect of the textview.
    CGRect visibleRect = self.textView.frame;

    //We will scroll only if the caret falls outside of the visible rect.
    if (!CGRectContainsRect(visibleRect, caretRect))
    {
        // Work out how much the scroll position would have to change by to make the cursor visible
        CGFloat diff = (caretRect.origin.y + caretRect.size.height) - (visibleRect.origin.y + visibleRect.size.height);

        // If diff < 0 then this isn't to do with the iOS7 bug, so ignore
        if (diff > 0) {
            // Scroll just enough to bring the cursor back into view
            CGPoint newOffset = self.textView.contentOffset;
            newOffset.y += diff;
            [self.textView setContentOffset:newOffset animated:YES];
        }
    }
}

对我来说就像一个魅力

【讨论】:

  • 您定义“差异”的行是正确的。但是,我建议也将 _textView.font.lineHeight*2 添加到 diff 中,否则如果用户在 textView 底部输入一个空行,部分光标将最终离开屏幕。
【解决方案3】:

已经有很多答案了,我发现在我的情况下它实际上要简单得多。在 keyboardWillShow 上,我调整了文本视图的 contentInset 并保持框架全屏。虽然scrollRangeToVisible: 不像其他许多人那样对我有用,但滚动视图方法(从 UITextView 继承)工作得很好。这对我有用:

- (void)textViewDidChange:(UITextView *)textView
{
    CGRect caret = [_textView caretRectForPosition:_textView.selectedTextRange.end];
    [_textView scrollRectToVisible:caret animated:YES];
}

【讨论】:

  • 这对我来说是一个很好的答案。我的 UITextView 在 UIScrollView 内,所以我不仅要获得插入符号的位置,还要在我的 UIScrollView 滚动到正确的 UITextView 光标位置之前附加 textView 的框架原点 y 位置。
【解决方案4】:

Anders 和 Leo Natan 有很好的解决方案。但是,我需要稍微修改他们的答案以使滚动与 contentInset 一起正常工作。我面临的问题是 textViewDidBeginEditing:keyboardWasShown: 之前被调用,因此 contentInset 更改不会在第一次通过时得到反映。这是我所做的:

在.h中

@interface NoteDayViewController : UIViewController <UITextViewDelegate>
{
    UIEdgeInsets noteTextViewInsets;
    UIEdgeInsets noteTextViewScrollIndicatorInsets;
    CGRect oldRect;
    NSTimer *caretVisibilityTimer;
    float noteViewBottomInset;
}
@property (weak, nonatomic) IBOutlet UITextView *noteTextView;

在.m中

- (void)registerForKeyboardNotifications
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWasShown:)
                                             name:UIKeyboardDidShowNotification object:nil];

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

- (void)keyboardWasShown:(NSNotification*)aNotification
{
    CGFloat kbHeight = // get the keyboard height following your usual method

    UIEdgeInsets contentInsets = noteTextViewInsets;
    contentInsets.bottom = kbHeight;
    noteTextView.contentInset = contentInsets;

    UIEdgeInsets scrollInsets = noteTextViewScrollIndicatorInsets;
    scrollInsets.bottom = kbHeight;
    noteTextView.scrollIndicatorInsets = scrollInsets;

    [noteTextView setNeedsDisplay];
}

- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{    
    noteTextView.contentInset = noteTextViewInsets;
    noteTextView.scrollIndicatorInsets = noteTextViewScrollIndicatorInsets;   
    [noteTextView setNeedsDisplay];
}

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    oldRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end];
    noteViewBottomInset = noteTextView.contentInset.bottom;
    caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES];
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    [caretVisibilityTimer invalidate];
    caretVisibilityTimer = nil;
}

- (void)scrollCaretToVisible
{
    // This is where the cursor is at.
    CGRect caretRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end];

    // test if the caret has moved OR the bottom inset has changed
    if(CGRectEqualToRect(caretRect, oldRect) && noteViewBottomInset == noteTextView.contentInset.bottom)
    return;

    // reset these for next time this method is called
    oldRect = caretRect;
    noteViewBottomInset = noteTextView.contentInset.bottom;

    // this is the visible rect of the textview.
    CGRect visibleRect = noteTextView.bounds;
    visibleRect.size.height -= (noteTextView.contentInset.top + noteTextView.contentInset.bottom);
    visibleRect.origin.y = noteTextView.contentOffset.y;

    // We will scroll only if the caret falls outside of the visible rect.
    if (!CGRectContainsRect(visibleRect, caretRect))
    {
        CGPoint newOffset = noteTextView.contentOffset;
        newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height, 0);
        [noteTextView setContentOffset:newOffset animated:NO]; // must be non-animated to work, not sure why
    }
}

【讨论】:

    【解决方案5】:

    这就是我最终做的事情,而且似乎可行:

    - (void)textViewKeyboardWillShow:(NSNotification *)notification
    {
    
        NSDictionary* info = [notification userInfo];
        CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    
         // self.textViewBottomSpace.constant = NSLayoutConstraint in IB (bottom position)
        self.textViewBottomSpace.constant = kbSize.height + 70;
        [self.textView setNeedsDisplay];
    }
    
    
    - (void)textViewKeyboardWillHide:(NSNotification *)notification
    {
        self.textViewBottomSpace.constant = 0;
        [self.textView setNeedsDisplay];
    }
    
    - (void)scrollCaretToVisible
    {
        //This is where the cursor is at.
        CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
    
        if(CGRectEqualToRect(caretRect, _oldRect))
            return;
    
        _oldRect = caretRect;
    
        //This is the visible rect of the textview.
        CGRect visibleRect = self.textView.bounds;
        visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
        visibleRect.origin.y = self.textView.contentOffset.y;
    
        //We will scroll only if the caret falls outside of the visible rect.
        if(!CGRectContainsRect(visibleRect, caretRect)) {
            CGPoint newOffset = self.textView.contentOffset;
    
            newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 10, 0);
    
            [self.textView setContentOffset:newOffset animated:YES];
        }
    }
    
    - (void)textViewDidEndEditing:(UITextView *)textView
    {    
        [_caretVisibilityTimer invalidate];
        _caretVisibilityTimer = nil;
    }
    
    - (void)textViewDidBeginEditing:(UITextView *)textView
    { 
        self.oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
        self.caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES];
    }
    

    【讨论】:

    • 嗨安德斯,你能解释一下这一行 self.textviewBottomSpace.constant 我似乎无法在你的代码 sn-p 中找到它的声明。谢谢!
    • 好吧,酷!用老式的方式改变框架应该有同样的效果。
    • 我可以确认此解决方案适用于调整帧大小的方法。
    【解决方案6】:

    此问题的更简单解决方案是更新文本视图框架以响应 textViewDidBegingEditing 委托方法。有关详细信息,请参阅以下内容:

    How to re-size UITextView when keyboard shown with iOS 7

    【讨论】:

    • 经过测试,但由于某种原因得到了与我原来的问题相同的结果。
    • @Anders 这真的很奇怪。这是一个非常令人沮丧的错误!我在这个上浪费了几个小时。
    【解决方案7】:

    对于那些在 UIScrollView 内有 UITextView 的用户,iOS

    // This is the scroll view reference
    @property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
    
    // Track the current UITextView
    @property (weak, nonatomic) UITextView *activeField;
    
    - (void)textViewDidBeginEditing:(UITextView *)textView
    {
        self.activeField = textView;
    }
    
    - (void)textViewdDidEndEditing:(UITextView *)textView
    {
        self.activeField = nil;
    }
    
    // Setup the keyboard observers that take care of the insets & initial scrolling
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWasShown:)
                                                 name:UIKeyboardDidShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillBeHidden:)
                                                 name:UIKeyboardWillHideNotification object:nil];
    
    - (void)keyboardWasShown:(NSNotification*)aNotification
    {
        // Set the insets above the keyboard
        NSDictionary* info = [aNotification userInfo];
        CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
    
        UIEdgeInsets insets = self.vForm.contentInset;
        insets.bottom += kbSize.height;
        self.vForm.contentInset = insets;
    
        insets = self.vForm.scrollIndicatorInsets;
        insets.bottom += kbSize.height;
        self.vForm.scrollIndicatorInsets = insets;
    
        // Scroll the active text field into view
        CGRect aRect = self.vForm.frame;
        aRect.size.height -= kbSize.height;
        CGPoint scrollPoint = CGPointMake(0.0, self.activeField.frame.origin.y);
        [self.scrollView setContentOffset:scrollPoint animated:YES];
    }
    
    - (void)keyboardWillBeHidden:(NSNotification*)aNotification
    {
        UIEdgeInsets contentInsets = UIEdgeInsetsZero;
        self.vForm.contentInset = contentInsets;
        self.vForm.scrollIndicatorInsets = contentInsets;
    }
    
    // This is where the magic happens. Set the class with this method as the UITextView's delegate.
    - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
        // Scroll the textview to the caret position
        [textView scrollRangeToVisible:textView.selectedRange];
    
        // Scroll the scrollview to the caret position within the textview
        CGRect targetRect = [textView caretRectForPosition:textView.selectedTextRange.end];
        targetRect.origin.y += self.activeField.frame.origin.y;
        [self.scrollView scrollRectToVisible:targetRect animated:YES];
    
        return YES;
    }
    

    我尝试包含大部分所需的胶水代码。唯一缺少的是设置 UITextView 的委托和关闭键盘。

    花了 2 到 3 天的时间来弄清楚以前的工作。谢谢,苹果。

    【讨论】:

      【解决方案8】:

      Angel Naydenov 上面的评论是对的,尤其是在从英文切换到日文键盘等显示提示的情况下。

      切换键盘时,会调用UIKeyboardWillShowNotification,但不会调用UIKeyboardWillHideNotification

      所以你必须调整插图使用绝对值而不是使用+=

      不相关,[self.textView setContentOffset:newOffset animated:YES]; 在第二次显示键盘后实际上不会改变 iOS 7.1 中的图形,这可能是一个错误。我使用的解决方法是替换

      [self.textView setContentOffset:newOffset animated:YES]; 
      

      [UIView animateWithDuration:.25 animations:^{
              self.textView.contentOffset = newOffset;
       }];
      

      【讨论】:

        【解决方案9】:

        Leo Natan,您的起步不错,但执行效率相对较低。这是一种使用更少代码的更好方法:

        // Add Keyboard Notification Listeners in ViewDidLoad
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
        
        
        // And Add The Following Methods
        - (void)_keyboardWillShowNotification:(NSNotification*)notification
        {    
            CGRect textViewFrame = self.textView.frame;
            textViewFrame.size.height -= ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0);
            self.textView.frame = textViewFrame;
        }
        
        - (void)_keyboardWillHideNotification:(NSNotification*)notification
        {
            CGRect textViewFrame = self.textView.frame;
            textViewFrame.size.height += ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0);
            self.textView.frame = textViewFrame;
        }
        
        - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
        
            NSRange typingRange = NSMakeRange(textView.text.length - 1, 1);
            [textView scrollRangeToVisible:typingRange];
        
            return YES;
        
        }
        
        - (void)dealloc {
        
            [[NSNotificationCenter defaultCenter] removeObserver:self];
        
        }
        

        【讨论】:

        • 这不是 Leo Natan 解决方案的更有效实现,它是一种不同的解决方案,它没有解决 Leo 的 iOS 7 键盘透明度问题。正如 Leo 所说,使用 contentInset 和 scrollIndicatorInsets 是更“正确”的解决方案,而不是仅仅改变帧大小。
        猜你喜欢
        • 2015-03-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多