Trying to make a UIScrollView scroll to a UITextView's cursor position

Discussion in 'iPhone/iPad Programming' started by Mr Skills, May 12, 2011.

  1. macrumors 6502a

    Mr Skills

    Nov 21, 2005
    I have a view which is similar to the notes app - i.e. typing on a lined piece of paper. To make the text and the paper scroll simultaneously, I have disabled the UITextView's scrolling, and instead placed both my UITextView and my UIImageView inside a UIScrollView.

    The only problem with this is that, when the user types, the text disappears below the keyboard, because obviously the UIScrollView does not know to scroll to the cursor position.

    Starting from something similar here (where someone was trying to do something similar with a UITableView), I have managed to make a growing, editable UITextView with a fixed background that almost scrolls perfectly with the cursor. The only issues now are:

    1. There is a slight judder as the text moves up if the user types particularly fast.
    2. If the user hides the keyboard, selects text at the bottom of the screen, and then shows the keyboard again, they have to type a couple of letters before the text becomes visible again - it doesn't scroll up immediately.
    3. When the user hides the keyboard, the animation as the scroll view's frame fills the screen doesn't feel quite right somehow.

    Here is the code - I'm sure it will be useful to some people, and I'd be really grateful if anyone can suggest how to refine it further and fix the problems above...

    #import "NoteEditViewController.h"
    #import "RLWideLabelTableCell.h"
    @implementation NoteEditViewController
    @synthesize keyboardSize;
    @synthesize keyboardHideDuration;
    @synthesize scrollView;
    @synthesize noteTextView;
    // Dealloc and all that stuff
    - (void)loadView
        [super loadView];
        UIScrollView *aScrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
        self.scrollView = aScrollView; [aScrollView release];
        self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width, noteTextView.frame.size.height);
        [self.view addSubview:scrollView];
    - (void)viewDidLoad
        [super viewDidLoad];
        // Get notified when keyboard is shown. Don't need notification when hidden because we are
        // using textViewDidEndEditing so we can start animating before the keyboard disappears.
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                     name:UIKeyboardDidShowNotification object:nil];
        // Add the Done button so we can test dismissal of the keyboard    
        UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 
        self.navigationItem.rightBarButtonItem = doneButton; [doneButton release];
        // Add the background image that will scroll with the text
        CGRect noteImageFrame = CGRectMake(self.view.bounds.origin.x, 
                                           self.view.bounds.size.width, 500);    
        UIView *backgroundPattern = [[UIView alloc] initWithFrame:noteImageFrame];
        backgroundPattern.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"Notepaper-iPhone-Line"]];
        [self.scrollView addSubview:backgroundPattern];
        [self.view sendSubviewToBack:backgroundPattern];
        [backgroundPattern release];
        // Add the textView
        CGRect textViewFrame = CGRectMake(noteImageFrame.origin.x+27, 
        RLTextView *textView = [[RLTextView alloc] initWithFrame:textViewFrame];
        self.noteTextView = textView; [textView release];
        self.noteTextView.font = [UIFont fontWithName:@"Cochin" size:21];
        self.noteTextView.backgroundColor = [UIColor clearColor];
        self.noteTextView.delegate = self;
        self.noteTextView.scrollEnabled = NO;
        [self.scrollView addSubview:self.noteTextView];
    - (void)doneButton:(id)sender
        [self.view endEditing:TRUE];
    // When the keyboard is shown, the UIScrollView's frame shrinks so that it fits in the
    // remaining space
    - (void)keyboardWasShown:(NSNotification*)aNotification
        NSDictionary* info = [aNotification userInfo];
        CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
        float kbHideDuration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
        self.keyboardHideDuration = kbHideDuration;
        self.keyboardSize = kbSize;
        self.scrollView.frame = CGRectMake(self.view.bounds.origin.x, 
                                           self.view.bounds.size.height - kbSize.height);    
    // When the user presses 'done' the UIScrollView expands to the size of its superview
    // again, as the keyboard disappears.
    - (void)textViewDidEndEditing:(UITextView *)textView
        [UIScrollView animateWithDuration:keyboardHideDuration animations:^{self.scrollView.frame = self.view.bounds;}];
    // This method needs to get called whenever there is a change of cursor position in the text box
    // That means both textViewDidChange: and textViewDidChangeSelection:
    - (void)scrollToCursor
        // if there is a selection cursor…
        if(noteTextView.selectedRange.location != NSNotFound) {
            NSLog(@"selectedRange: %d %d", noteTextView.selectedRange.location, noteTextView.selectedRange.length);
            // work out how big the text view would be if the text only went up to the cursor
            NSRange range;
            range.location = noteTextView.selectedRange.location;
            range.length = noteTextView.text.length - range.location;
            NSString *string = [noteTextView.text stringByReplacingCharactersInRange:range withString:@""];
            CGSize size = [string sizeWithFont:noteTextView.font constrainedToSize:noteTextView.bounds.size lineBreakMode:UILineBreakModeWordWrap];
            // work out where that position would be relative to the textView's frame
            CGRect viewRect = noteTextView.frame;  
            int scrollHeight = viewRect.origin.y + size.height;
            CGRect finalRect = CGRectMake(1, scrollHeight, 1, 1);
            // scroll to it
            [self.scrollView scrollRectToVisible:finalRect animated:YES];
    // Whenever the text changes, the textView's size is updated (so it grows as more text
    // is added), and it also scrolls to the cursor.
    - (void)textViewDidChange:(UITextView *)textView
        noteTextView.frame = CGRectMake(noteTextView.frame.origin.x, 
        self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 
        [self scrollToCursor];
    // The textView scrolls to the cursor whenever the user changes the selection point.
    - (void)textViewDidChangeSelection:(UITextView *)aTextView 
        [self scrollToCursor];
    // PROBLEM - the textView does not scroll until the user starts typing - just selecting
    // it is not enough. 
    - (void)textViewDidBeginEditing:(UITextView *)textView
        [self scrollToCursor];
  2. macrumors newbie

    Jun 16, 2011
    Fix for problem #2

    Mr Skills:

    Thanks for posting your code, it was extremely helpful, particularly the bit where you calculate the height of the text up until the cursor.

    I too encountered the problems you've mentioned here, and I can suggest a fix to one of them:

    This is pretty easy to fix. What you need to do is subscribe the View Controller to the keyboardWillShow system notification. Then, in the notification callback method, you can call scrollToCursor. This callback fires regardless of whether the user has started typing or not.

    To subscribe to the Notification, I put the following inside viewDidLoad:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardDidShowNotification object:nil];
    Then inside the keyboardWillShow method, just call scrollToCursor.

    (I've also subscribed to the keyboardWillBeHidden Notification in the same way to execute resizing after the keyboard is dismissed).

    I'm also seeing the "juddering" you mention in #1. Its not terrible, but its not great either. For me it seems to happen every time a new line begins in the textView. I'd love to hear if you (or anyone else) was able to correct this.

    As for problem #3, could you elaborate on what "doesn't quite feel right" with the animation? Is it the timing, or the actual motion itself?
  3. macrumors newbie

    Jul 9, 2011
    Fix for #1

    Hi Mr Skills,

    Thank you very much for your nice codes.

    For following problem,
    When I changed the height of finalRect as follows, the slight judder disappeared:
    CGSize size2 = [@" " sizeWithFont:self.currentTextView.font
    // work out where that position would be relative to the textView's frame
    CGRect viewRect = self.currentTextView.frame;
    int scrollHeight = viewRect.origin.y + size.height + size2.height;
    CGRect finalRect = CGRectMake(1, scrollHeight, 1, size2.height);
    I hope this would be helpful to you and others.

Share This Page