102

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.

Kampai
  • 22,848
  • 21
  • 95
  • 95
Sam Stewart
  • 1,470
  • 3
  • 14
  • 14

37 Answers37

152
UITextView*note;
[note setContentOffset:CGPointZero animated:YES];

This does it for me.

Swift version (Swift 4.1 with iOS 11 on Xcode 9.3):

note.setContentOffset(.zero, animated: true)
keeshux
  • 592
  • 3
  • 10
Wayne Lo
  • 3,689
  • 2
  • 28
  • 28
68

If anyone has this problem in iOS 8, I found that just setting the UITextView's text property, then calling scrollRangeToVisible with an NSRange with location:0, length:0, worked. My text view was not editable, and I tested both selectable and not selectable (neither setting affected the result). Here's a Swift example:

myTextView.text = "Text that is long enough to scroll"
myTextView.scrollRangeToVisible(NSRange(location:0, length:0))
iGatiTech
  • 2,306
  • 1
  • 21
  • 45
nerd2know
  • 844
  • 6
  • 5
46

And here is my solution...

    override func viewDidLoad() {
        textView.scrollEnabled = false
        textView.text = "your text"
    }

    override func viewDidAppear(animated: Bool) {
        textView.scrollEnabled = true
    }
miguel
  • 461
  • 4
  • 2
  • 5
    I don't love this solution, but at least as of iOS 9 beta 5, it's the only thing that works for me. I'm assuming most of the other solutions are assuming your text view is already visible. In my case that's not always true, so I have to keep scrolling disabled until after it appears. – robotspacer Aug 10 '15 at 20:53
  • 1
    This was the only thing that worked for me too. iOS 9, using a non-editable textview. – SeanR Oct 12 '15 at 10:28
  • 1
    This works for me, iOS 8 non-editable textview with attributed string. In viewWillAppear - does not work – Tina Zh Jan 25 '16 at 16:49
  • I agree with @robotspacer. and this solution is still the only one that works for me. – lerp90 Jul 13 '16 at 18:32
  • For me, this solution only worked with short (attributed) texts. When having longer texts, the UITextView scroll bug keeps staying alive :-) My (a little ugly) solution was to use dispatch_after with a 2 seconds delay to reenable scrolling. UITextView seems to need a small amount of time to do its text layout... – LaborEtArs Aug 12 '16 at 19:22
39

Calling

scrollRangeToVisible(NSMakeRange(0, 0))

works but call it in

override func viewDidAppear(animated: Bool)
RyanTCB
  • 7,400
  • 5
  • 42
  • 62
29

I was using attributedString in HTML with text view not editable. Setting the content offset did not work for me either. This worked for me: disable scroll enabled, set the text and then enable the scrolling again

[yourTextView setScrollEnabled:NO];
yourTextView.text = yourtext;
[yourTextView setScrollEnabled:YES];
Maria
  • 4,471
  • 1
  • 25
  • 26
  • I had a textView inside a tableViewCell. In this case the textView got scrolled to approx. 50%. This solution is as simple as logical. Thanks – Julian F. Weinert Aug 13 '15 at 10:29
  • 1
    @JulianF.Weinert This does not seem to work for iOS10. I have the same situation as you with the textView in a tableViewCell. I am not using the editing capability of the textView. I just want scrollable text. But the textView is always scrolled to approx 50% as you said. I've also tried all the other suggestions with setting the setContentOffset and scrollRangeToVisible in this post. None work. Is there anything else you may have done to make this work? – JeffB6688 May 08 '17 at 14:14
  • @JeffB6688 sorry, no. That's really a long time ago... Might be another quirk of a "new" iOS release? – Julian F. Weinert May 08 '17 at 14:27
  • But not perfectly on iOS 11.2 unfortunately. Onlu @RyanTCB 's answer works on both iOS 10 and iOS 11. – Sipke Schoorstra Dec 07 '17 at 15:36
  • This works fine for me so far on iOS 9, 10 and 11, but I had to enable it in `viewDidAppear` as part of a broader serious of fixes – MadProgrammer Mar 20 '18 at 21:15
19

Since none of these solutions worked for me and I wasted way too much time piecing together solutions, this finally solved it for me.

override func viewWillAppear(animated: Bool) {
    dispatch_async(dispatch_get_main_queue(), {
        let desiredOffset = CGPoint(x: 0, y: -self.textView.contentInset.top)
        self.textView.setContentOffset(desiredOffset, animated: false)
    })
}

This is really silly that this is not default behavior for this control.

I hope this helps someone else out.

Drew S.
  • 516
  • 4
  • 19
  • 2
    This was very helpful! I had to also add: self.automaticallyAdjustsScrollViewInsets = NO; to the view containing the UITextView to get it to fully work. – jengelsma Jan 19 '16 at 20:19
  • I only made it work in SWIFT 3, with another offset calc `self.textView.contentOffset.y` based on this answer: https://stackoverflow.com/a/25366126/1736679 – Efren Jul 27 '17 at 23:23
  • Worked for me once I adapted to the latest syntax for Swift 3 – hawmack13 Sep 14 '17 at 09:15
  • totally agree how crazy is this? thanks for the solution – ajonno Jun 07 '19 at 05:53
18

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

Ben Sussman
  • 981
  • 8
  • 10
  • 1
    @SamStewart The link seems to be broken, always leads to my accounts page (already logged in) can you provide more info on what solution they offer there? – uliwitness Jul 17 '17 at 11:56
  • @uliwitness This is *super* old information now, I would recommend trying a more modern resource instead of a 8 year old post. The iOS API has changed dramatically in those 8 years. – Ben Sussman Jul 18 '17 at 17:04
  • 1
    I have tried to find newer info. If you know where I can find it, I'd appreciate that. Still seems to be the same issue :( – uliwitness Jul 19 '17 at 09:17
  • Thanks this worked for me because I have also set insets in the User Defined Runtime Attributes `TextView.scrollRectToVisible(CGRect(x: 0,y: 0,width: 1,height: 1), animated: false)` – Kurt L. Aug 27 '20 at 07:30
14

For iOS 10 and Swift 3 I did:

override func viewDidLoad() {
    super.viewDidLoad()
    textview.isScrollEnabled = false
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    textView.isScrollEnabled = true
}

Worked for me, not sure if you're having the problem with the latest version of Swift and iOS.

Wilson
  • 9,006
  • 3
  • 42
  • 46
ChallengerGuy
  • 2,385
  • 6
  • 27
  • 43
  • A perfect solution to follow! – Chirag Lukhi Dec 21 '17 at 12:43
  • I'm having the scrolling issue with a UITextView in a UIView controlled by a UIPageViewController. (These pages show my Help/About screens.) Your solution works for every page except the first one. Swiping to any page and back to the first page fixes the scroll position of that first page. I tried adding another .isScrollEnabled = False into the viewWillAppear but this had no effect. I'm stumped as to why the first page behaves differently than the rest. – Wayne Henderson Jan 06 '18 at 19:19
  • [update] Just solved it! Introducing a delay of 0.5 seconds before the .isScrollEnabled = true triggers fixes the first-load problem. – Wayne Henderson Jan 06 '18 at 19:55
  • very nice . . . – Maysam R Jan 31 '18 at 22:44
  • Worked like a charm. The only solution that works for me. You are the man. – Lazy Ninja Jul 13 '18 at 09:58
10

This is how it worked on iOS 9 Release so as the textView is scrolled on top before appearing on screen

- (void)viewDidLoad
{
    [super viewDidLoad];
    textView.scrollEnabled = NO;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    textView.scrollEnabled = YES;
}
ageorgios
  • 323
  • 2
  • 7
  • 2
    This did the trick. Incidentally, it taught me something very important: the 'view' in 'viewwillappear' doesn't just refer to self.view, but apparently to ALL views. No? – Sjakelien Dec 15 '17 at 17:29
10

I have an updated answer which will have the textview appear properly, and without the user experiencing a scroll animation.

override func viewWillAppear(animated: Bool) {
    dispatch_async(dispatch_get_main_queue(), {
        self.introductionText.scrollRangeToVisible(NSMakeRange(0, 0))
    })
MacInnis
  • 760
  • 9
  • 19
9

That's how I did it

Swift 4:

extension UITextView {

    override open func draw(_ rect: CGRect)
    {
        super.draw(rect)
        setContentOffset(CGPoint.zero, animated: false)
    }

 }
8

This worked for me

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

    DispatchQueue.main.async {
      self.textView.scrollRangeToVisible(NSRange(location: 0, length: 0))
    }
  }
garg
  • 2,651
  • 1
  • 24
  • 21
  • This is very weird. On most devices, wrapping the call in `DispatchQueue.main.async(...` isn't necessary, but for some reason, on (e.g.) iPhone 5s (iOS 12.1) it doesn't work unless you dispatch. But as reported by the call stack on the debugger, `viewWillAppear()` **is already running on the main queue** in all cases... - WTF, Apple??? – Nicolas Miari May 20 '19 at 03:58
  • 2
    I was also wondering why I have to dispatch explicitly on the main queue even though I am running all events on the main queue. But this worked for me – garg May 20 '19 at 05:37
7

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

    myTextView.setContentOffset(.zero, animated: true)
Charlie S
  • 4,366
  • 6
  • 59
  • 97
6

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.

John Ballinger
  • 7,380
  • 5
  • 41
  • 51
6

My issue is to set textView to scroll to top when view appeared. For iOS 10.3.2, and Swift 3.

Solution 1:

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

    // It doesn't work. Very strange.
    self.textView.setContentOffset(CGPoint.zero, animated: false)
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    // It works, but with an animation effection.
    self.textView.setContentOffset(CGPoint.zero, animated: false)
}

Solution 2:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // It works.       
    self.textView.contentOffset = CGPoint.zero
}
AechoLiu
  • 17,522
  • 9
  • 100
  • 118
6

just you can use this code

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    textView.scrollRangeToVisible(NSMakeRange(0, 0))
}
erkancan
  • 61
  • 1
  • 2
5

Combined the previous answers, now it should work:

talePageText.scrollEnabled = false
talePageText.textColor = UIColor.blackColor()
talePageText.font = UIFont(name: "Bradley Hand", size: 24.0)
talePageText.contentOffset = CGPointZero
talePageText.scrollRangeToVisible(NSRange(location:0, length:0))
talePageText.scrollEnabled = true
Bowdzone
  • 3,827
  • 11
  • 39
  • 52
Géza Mikló
  • 81
  • 1
  • 6
5
[txtView setContentOffset:CGPointMake(0.0, 0.0) animated:YES];

This line of code works for me.

dustinrwh
  • 888
  • 1
  • 14
  • 16
Patricia
  • 289
  • 3
  • 6
4

Swift 2 Answer:

textView.scrollEnabled = false

/* Set the content of your textView here */

textView.scrollEnabled = true

This prevents the textView from scrolling to the end of the text after setting it.

glace
  • 2,162
  • 1
  • 17
  • 23
4

In Swift 3:

  • viewWillLayoutSubviews is the place to make changes. viewWillAppear should work as well, but logically, layout should be perform in viewWillLayoutSubviews.

  • Regarding methods, both scrollRangeToVisible and setContentOffset will work. However, setContentOffset allows animation to be off or on.

Assume the UITextView is named as yourUITextView. Here's the code:

// Connects to the TextView in the interface builder.
@IBOutlet weak var yourUITextView: UITextView!

/// Overrides the viewWillLayoutSubviews of the super class.
override func viewWillLayoutSubviews() {

    // Ensures the super class is happy.
    super.viewWillLayoutSubviews()

    // Option 1:
    // Scrolls to a range of text, (0,0) in this very case, animated.
    yourUITextView.scrollRangeToVisible(NSRange(location: 0, length: 0))

    // Option 2:
    // Sets the offset directly, optional animation.
    yourUITextView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
}
Marco Leong
  • 565
  • 4
  • 11
3

This worked for me. It is based on RyanTCBs answer but it is the Objective-C variant of the same solution:

- (void) viewWillAppear:(BOOL)animated {
    // For some reason the text view, which is a scroll view, did scroll to the end of the text which seems to hide the imprint etc at the beginning of the text.
    // On some devices it is not obvious that there is more text when the user scrolls up.
    // Therefore we need to scroll textView to the top.
    [self.textView scrollRangeToVisible:NSMakeRange(0, 0)];
}
Hermann Klecker
  • 14,039
  • 5
  • 48
  • 71
3
-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];

    [textview setContentOffset:CGPointZero animated:NO];
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Albert Zou
  • 165
  • 2
  • 4
3
-(void) viewDidAppear:(BOOL)animated{
[mytextView scrollRangeToVisible:NSMakeRange(0,0)];}

- (void)viewWillAppear:(BOOL)animated{
[mytextView scrollRangeToVisible:NSMakeRange(0,0)];}
  • ViewWillappear will do it instantly before user can notice, but ViewDidDisappear section will animate it lazily.
  • [mytextView setContentOffset:CGPointZero animated:YES]; does NOT work sometimes(I don't know why), so use scrollrangetovisible rather.
Jagat Dave
  • 1,643
  • 3
  • 23
  • 30
Kahng
  • 189
  • 1
  • 8
2
[self.textView scrollRectToVisible:CGRectMake(0, 0, 320, self.textView.frame.size.height) animated:NO];
shyla
  • 1,430
  • 1
  • 13
  • 10
2

Problem solved: iOS = 10.2 Swift 3 UITextView

I just used the following line:

displayText.contentOffset.y = 0
  • 1
    Worked (iOS 10.3)! I added it inside `viewDidLayoutSubviews` so it happens when the screen rotates. Otherwise the content offset becomes misaligned. – Pup May 27 '17 at 18:37
2

I had the problem, but in my case textview is the subview of some custom view and added it to ViewController. None of the solution worked for me. To solve the problem added 0.1 sec delay and then enabled scroll. It worked for me.

textView.isScrollEnabled = false
..
textView.text = // set your text here

let dispatchTime = DispatchTime.now() + 0.1
DispatchQueue.main.asyncAfter(deadline: dispatchTime) {
   self.textView.isScrollEnabled = true
}
SaRaVaNaN DM
  • 4,390
  • 4
  • 22
  • 30
1

For me fine works this code:

    textView.attributedText = newText //or textView.text = ...

    //this part of code scrolls to top
    textView.contentOffset.y = -64
    textView.scrollEnabled = false
    textView.layoutIfNeeded() //if don't work, try to delete this line
    textView.scrollEnabled = true

For scroll to exact position and show it on top of screen I use this code:

    var scrollToLocation = 50 //<needed position>
    textView.contentOffset.y = textView.contentSize.height
    textView.scrollRangeToVisible(NSRange.init(location: scrollToLocation, length: 1))

Setting contentOffset.y scrolls to the end of text, and then scrollRangeToVisible scrolls up to value of scrollToLocation. Thereby, needed position appears in first line of scrollView.

Igor
  • 12,165
  • 4
  • 57
  • 73
1

For me in iOS 9 it stopped to work for me with attributed string with method scrollRangeToVisible (the 1 row was with bold font, other rows were with regular font)

So I had to use:

textViewMain.attributedText = attributedString
textViewMain.scrollRangeToVisible(NSRange(location:0, length:0))
delay(0.0, closure: { 
                self.textViewMain.scrollRectToVisible(CGRectMake(0, 0, 10, 10), animated: false)
            })

where delay is:

func delay(delay:Double, closure:()->Void) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}
Paul T.
  • 4,938
  • 7
  • 45
  • 93
1

Try to use this 2 lines solution:

view.layoutIfNeeded()
textView.contentOffset = CGPointMake(textView.contentInset.left, textView.contentInset.top)
Nikolay Shubenkov
  • 3,133
  • 1
  • 29
  • 31
  • This worked for me. My UITextView was inside a UICollectionVewCell. I suspect that scrollRangeToVisible scrolls to the wrong spot unless layout is computed properly – John Fowler Oct 19 '16 at 05:33
1

Here are my codes. It works fine.

class MyViewController: ... 
{
  private offsetY: CGFloat = 0
  @IBOutlet weak var textView: UITextView!
  ...

  override viewWillAppear(...) 
  {
      ...
      offsetY = self.textView.contentOffset.y
      ...
  }
  ...
  func refreshView() {
      let offSetY = textView.contentOffset.y
      self.textView.setContentOffset(CGPoint(x:0, y:0), animated: true)
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
        [unowned self] in
        self.textView.setContentOffset(CGPoint(x:0, y:self.offSetY), 
           animated: true)
        self.textView.setNeedsDisplay()
      }
  }
David.Chu.ca
  • 37,408
  • 63
  • 148
  • 190
1

Swift 3 version of drew_s answer earlier, which is the only thing that worked for me after trying most of the other answers.

    override func viewWillAppear(_ animated: Bool) {
    DispatchQueue.main.async{
        let desiredOffset = CGPoint(x: 0, y: -self.person_description.contentInset.top)
        self.person_description.setContentOffset(desiredOffset, animated: false)
    }
}

This was a cut and paste of working code. Replace person_description with your textview variable.

hawmack13
  • 233
  • 2
  • 17
1

I think the reason why some people are having problems with this issue is because they are trying to set the TextView scroll position, before this TextView has appeared.

There is nothing to scroll, when the TextView hasn't yet appeared.

You need to wait for the TextView to appear first, and only then set its scroll position inside the viewDidAppear method as shown below:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    textView.setContentOffset(CGPoint.zero, animated: false)
}
1

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
Terry
  • 339
  • 4
  • 5
1

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.

Revanth Kausikan
  • 673
  • 1
  • 9
  • 21
0

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()
    }
}
Wayne Henderson
  • 237
  • 3
  • 9
0

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
        }
    }
}
Adam
  • 1,776
  • 1
  • 17
  • 28
-1

Solution for Xcode 8.0. Swift 2.3. Works for interface build and can be easily modified to work programmatically.

class MBTextView: UITextView
{
    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        setupView()
    }

    func setupView() {}
}

class MBStartFromTopTV: MBTextView
{
    override func setupView()
    {
        // some custom code
    }

    override func drawRect(rect: CGRect)
    {
        super.drawRect(rect)
        setContentOffset(CGPoint.zero, animated: false)
    }
}
Darkwonder
  • 1,149
  • 1
  • 13
  • 26
  • 1
    drawRect is for drawing a view, it is really bad style to do other stuff (like moving or adding views) in this method. It should really just draw. Also, this gets called whenever the OS thinks the view needs to be redrawn. Which is not that often, but this nukes the scroll position whenever the screen depth changes etc. (which could happen when e.g. an HDMI adapter is attached or whatever you can imagine) – uliwitness Jul 17 '17 at 12:02
  • The question was: How do I force the UITextView to scroll to the top every time I change the text? Not: What is the best solution / best way. The above solutions didn't work for me but mine did. I agree with you that there are better solutions but I disagree that mine doesn't work / is wrong. Thank you for the explanation. I will refactor my code and post an update if I find a better working solution. – Darkwonder Jul 17 '17 at 16:29