更改框架时,UITextView光标位于框架下方

时间:2013-08-25 17:26:42

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

我的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框架的“外部”。见下图:

enter image description here

黄色区域是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);
}

9 个答案:

答案 0 :(得分:20)

为什么不给文字视图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应该提供更好的处理插入符号的方法,但这很有效。

答案 1 :(得分:6)

我试过的所有其他答案对我来说都有点奇怪。使用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];
        }
    }
}

对我来说就像一个魅力

答案 2 :(得分:4)

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

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

答案 3 :(得分:3)

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
    }
}

答案 4 :(得分:1)

这就是我最终做的事情,以及似乎有效的事情:

- (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];
}

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

对于UITextViewUIScrollView的人,其中iOS&lt; 7负责将插入符号滚动到视图中:以下是iOS 7(以及5&amp; 6)的工作原理。

// 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天才弄明白以前的工作。谢谢,Apple。

答案 7 :(得分:1)

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;
 }];

答案 8 :(得分:0)

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];

}