UITextView cursor below frame when changing frame

前端 未结 9 420
名媛妹妹
名媛妹妹 2020-12-01 06:50

I have a UIViewCOntrollerthat contains a UITextView. When the keyboard appears I resize it like this:

#pragma mark - Responding to          


        
相关标签:
9条回答
  • 2020-12-01 07:20

    Leo Natan, you started out well but your execution was relatively inefficient. Here is a better way of doing it with less code:

    // 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];
    
    }
    
    0 讨论(0)
  • 2020-12-01 07:21

    Instead of resizing the frame, why not give your text view a contentInset (and a matching scrollIndicatorInsets)? Remember that text views are actually scrollviews. This is the correct way to handle keyboard (or other) interference.

    For more information on contentInset, see this question.


    This seems to not be enough. Still use insets, as this is more correct (especially on iOS7, where the keyboard is transparent), but you will also need extra handling for the caret:

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

    A lot of work, Apple should provide better way of handling the caret, but this works.

    0 讨论(0)
  • 2020-12-01 07:24

    Anders and Leo Natan have great solutions. However, I needed to modify their answers a little to get the scrolling to work properly with contentInset. The problem I faced was that textViewDidBeginEditing: gets called before keyboardWasShown: so the contentInset change does not get reflected the first time through. Here is what I did:

    In .h

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

    In .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
        }
    }
    
    0 讨论(0)
  • 2020-12-01 07:24

    Angel Naydenov's comment above is right, especially in cases such as switching from English to Japanese keyboard that shows suggests.

    When switching keyboards, UIKeyboardWillShowNotification is called but UIKeyboardWillHideNotification is not called.

    So you must adjust the inset to use the absolute value and not use +=.

    Unrelatedly, [self.textView setContentOffset:newOffset animated:YES]; will not actually change the graphics in iOS 7.1 after the keyboard is shown for the second time, which is probably a bug. A workaround I used is replacing

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

    with

    [UIView animateWithDuration:.25 animations:^{
            self.textView.contentOffset = newOffset;
     }];
    
    0 讨论(0)
  • 2020-12-01 07:31

    All of the others answers I tried behaved somewhat strangely for me. Using an NSTimer to perform the scroll also meant that the user couldn't scroll up, since the caret would then end up off-screen and it would immediately scroll back down again. In the end I stuck with the original approach of changing the UITextView frame on the keyboard notification events, then added the following methods:

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

    Works like a charm for me

    0 讨论(0)
  • 2020-12-01 07:33

    For those that have a UITextView inside a UIScrollView where iOS < 7 took care of scrolling the caret into view: Here's how it works with iOS 7 (and also 5 & 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;
    }
    

    I tried to include most of the required glue code. The only things missing are setting the UITextView's delegate and dismissing the keyboard.

    Took 2-3 days to figure out what previously worked. Thanks, Apple.

    0 讨论(0)
提交回复
热议问题