How do I force a UITextView to scroll to the top every time I change the text?

Viewed 73931

OK, I'm having some problem with the UITextView. Here's the issue:

I add some text to a UITextView. The user then double clicks to select something. I then change the text in the UITextView (programatically as above) and the UITextView scrolls to the bottom of the page where there is a cursor.

However, that is NOT where the user clicked. It ALWAYS scrolls to the bottom of the UITextView regardless of where the user clicked.

So here's my question: How do I force the UITextView to scroll to the top every time I change the text? I've tried contentOffset and scrollRangeToVisible. Neither work.

Any suggestions would be appreciated.

37 Answers

I'm not sure if I understand your question, but are you trying to simply scroll the view to the top? If so you should do

[textview scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];

This worked for me

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    DispatchQueue.main.async {
      self.textView.scrollRangeToVisible(NSRange(location: 0, length: 0))
    }
  }

I had to call the following inside viewDidLayoutSubviews (calling inside viewDidLoad was too early):

    myTextView.setContentOffset(.zero, animated: true)

Try this to move the cursor to the top of the text.

NSRange r  = {0,0};
[yourTextView setSelectedRange:r];

See how that goes. Make sure you call this after all your events have fired or what ever you are doing is done.

[txtView setContentOffset:CGPointMake(0.0, 0.0) animated:YES];

This line of code works for me.

This code solves the problem and also helps to avoid needless autoscroll to the end of textView. Just use method setTextPreservingContentOffset: instead of setting text to textView directly.

@interface ViewController () {
    __weak IBOutlet UITextView *textView;
    CGPoint lastContentOffset;
    NSTimer *autoscrollTimer;
    BOOL revertAnyContentOffsetChanges;
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [textView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)dealloc
{
    [textView removeObserver:self forKeyPath:@"contentOffset"];
}

- (void)setTextPreservingContentOffset:(NSString *)text
{
    lastContentOffset = textView.contentOffset;
    [autoscrollTimer invalidate];
    revertAnyContentOffsetChanges = YES;
    textView.text = text;
    autoscrollTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(enableScrolling:) userInfo:nil repeats:NO];
    autoscrollTimer.tolerance = 1;
}

- (void)enableScrolling:(NSTimer *)timer
{
    revertAnyContentOffsetChanges = NO;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (revertAnyContentOffsetChanges && [keyPath isEqualToString:@"contentOffset"] && [[change valueForKey:@"new"] CGPointValue].y != lastContentOffset.y) {
        [textView setContentOffset:lastContentOffset animated:NO];
    }
}

@end

The default behaviour of the textview is the scroll to the bottom, as it enables the users to continue editing.

Setting the textview offset as its inset's top value solves it for me.

override func viewDidLayoutSubviews() {  
    super.viewDidLayoutSubviews()  
    self.myTextView.contentOffset.y = -self.myTextView.contentInset.top  
}

Note: My textView was embedded inside a navigation controller.

The answer by ChallengerGuy fully solved my problem once I added a brief delay before re-enabling scrolling. Prior to adding the delay, my PageController pages were all fixed except for the first page, which would also fix itself after any user interaction with the page. Adding the delay fixed the UITextView scroll position of that first page as well.

override open func viewDidLoad() {
    super.viewDidLoad()
    textView.isScrollEnabled = false
    textView.text = textToScroll            
   ... other stuff
}

override open func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    Central().delay(0.5) {
        self.textView.isScrollEnabled = true
    }
}

(The delay function in my Central() file.)

func delay(_ delay: Double, closure:@escaping ()->()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        closure()
    }
}

For Swift I think the cleanest way to do this is to subclass UITextView and use this little trick

import UIKit

class MyTextView: UITextView {
    override var text: String! {
        willSet {
            isScrollEnabled = false
        } didSet {
            isScrollEnabled = true
        }
    }
}
Related