19

I have to update a small amount of text in a scrolling UITextView. I'll only be inserting a character where the cursor currently is, and I'll be doing this on a press of a button on my navigation bar.

My problem is that whenever I call the setText method of the text view, it jumps to the bottom of the text. I've tried using contentOffset and resetting the selectedRange but it doesn't work! Here's my example:

// Remember offset and selection
CGPoint contentOffset = [entryTextView contentOffset];
NSRange selectedRange = [entryTextView selectedRange];
// Update text
entryTextView.text = entryTextView.text;
// Try and reset offset and selection
[entryTextView setContentOffset:contentOffset animated:NO];
[entryTextView setSelectedRange: selectedRange];

Is there any way you can update the text without any scroll movement at all... as if they'd just typed something on the keyboard?

Edit:

I've tried using the textViewDidChange: delegate method but it's still not scrolling up to the original location.

- (void)textViewDidChange:(UITextView *)textView {
    if (self.programChanged) {
        [textView setSelectedRange:self.selectedRange];
        [textView setContentOffset:self.contentOffset animated:NO];
        self.programChanged = NO;
    }
}

- (void)changeButtonPressed:(id)sender {
    // Remember position
    self.programChanged = YES;
    self.contentOffset = [entryTextView contentOffset];
    self.selectedRange = [entryTextView selectedRange];
    // Update text
    entryTextView.text = entryTextView.text;
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Michael Waterfall
  • 20,497
  • 27
  • 111
  • 168

13 Answers13

16

If you use iPhone 3.0 or later, you can solve this problem:

textView.scrollEnabled = NO;

//You should know where the cursor will be(if you update your text by appending/inserting/deleting you can know the selected range) so keep it in a NSRange variable.

Then update text:
textView.text = yourText;

textView.scrollEnabled = YES;
textView.selectedRange = range;//you keep before

It should work now (no more jumping)

Regards Meir Assayag

BufferStack
  • 549
  • 9
  • 20
Mayosse
  • 696
  • 1
  • 7
  • 16
8

Building on Meir's suggestion, here's code that removes the selection programmatically (yes I know there's a selection menu button that does it too, but I'm doing something a bit funky) without scrolling the text view.

NSRange selectedRange = textView.selectedRange;
textView.scrollEnabled = NO;
// I'm deleting text. Replace this line with whatever insertions/changes you want
textView.text = [textView.text
                stringByReplacingCharactersInRange:selectedRange withString:@""];
selectedRange.length = 0;
// If you're inserting text, you might want to increment selectedRange.location to be
// after the text you inserted
textView.selectedRange = selectedRange;
textView.scrollEnabled = YES;
Jacques
  • 6,050
  • 1
  • 31
  • 24
  • 1
    This solution (setting scrollEnabled AFTER setting the selected range, not before) fixed a "jumping text" issue I was having when setting the attributedText property of a UITextView! Thanks – software evolved May 22 '13 at 16:02
6

This decision works for iOS 8:

let offset = textView.contentOffset
textView.text = newText
textView.layoutIfNeeded()
textView.setContentOffset(offset, animated: false)

It is necessary to call exactly setContentOffset:animated: because only this will cancel animation. textView.contentOffset = offset will not cancel the animation and will not help.

Anton Zherdev
  • 674
  • 1
  • 8
  • 10
4

The following two solutions don't work for me on iOS 8.0.

textView.scrollEnabled = NO;
[textView.setText: text];
textView.scrollEnabled = YES;

and

CGPoint offset = textView.contentOffset;
[textView.setText: text];
[textView setContentOffset:offset];

I setup a delegate to the textview to monitor the scroll event, and noticed that after my operation to restore the offset, the offset is reset to 0 again. So I instead use the main operation queue to make sure my restore operation happens after the "reset to 0" option.

Here's my solution that works for iOS 8.0.

CGPoint offset = self.textView.contentOffset;
self.textView.attributedText = replace;
[[NSOperationQueue mainQueue] addOperationWithBlock: ^{
    [self.textView setContentOffset: offset];
}];
Harper
  • 1,794
  • 14
  • 31
  • 1
    So, this fixed my issue when testing it in the simulator but did not fix the issue when running it on my device. This leads me to believe it is still a race condition. I am considering just subclassing uitextview and overriding the layoutsubviews method to not run when I have a lock property enabled. – Lobsterman Apr 07 '15 at 20:25
3

In order to edit the text of a UITextView, you need to update it's textStorage field:

[_textView.textStorage beginEditing];

NSRange replace = NSMakeRange(10, 2); //enter your editing range
[_textView.textStorage replaceCharactersInRange:replace withString:@"ha ha$ "];

//if you want to edit the attributes
NSRange attributeRange = NSMakeRange(10, 5); //enter your editing attribute range
[_textView.textStorage addAttribute:NSBackgroundColorAttributeName value:[UIColor greenColor] range:attributeRange];

[_textView.textStorage endEditing];

Good luck

Yedidya Reiss
  • 5,316
  • 2
  • 17
  • 19
3

No of the suggested solutions worked for me. -setContentOffset:animated: gets triggered by -setText: 3 times with animated YES and a contentOffset of the end (minus the default 8pt margin of a UITextView). I wrapped the -setText: in a guard:

textView.contentOffsetAnimatedCallsDisabled = YES;
textView.text = text;
textView.contentOffsetAnimatedCallsDisabled = NO;

In a UITextView subclass in -setContentOffset:animated: put

if (contentOffsetAnimatedCallsDisabled) return; // early return

among your other logic. Don’t forget the super call. This works.

Raphael

Raphael Schaad
  • 1,659
  • 1
  • 17
  • 17
  • 1
    It's not private API, you override UITextView and implement the behavior yourself according to my example. Meir Assayag's answer didn't work in my case and on iOS 3.2. – Raphael Schaad Aug 20 '10 at 16:30
  • On iOS 7 this does not work, since the contentOffset method will be called again at display time. I am looking for a working solution. – phatmann Sep 21 '13 at 21:17
2

in iOS 7. There seams to be a bug with sizeThatFits and having linebreaks in your UITextView the solution I found that works is to wrap it by disabling scrolling. Like this:

textView.scrollEnabled = NO;
CGSize newSize = [textView sizeThatFits:CGSizeMake(fixedWidth, MAXFLOAT)];
textView.scrollEnabled = YES;

and weird jumping has been fixed.

over_optimistic
  • 1,399
  • 2
  • 18
  • 27
  • On iOS 8 setting `textView.scrollEnabled = NO` in `textDidChange:` seems to disable scrolling forever. However on this OS it seems to be enough to just toggle `scrollEnabled` once during `viewDidLoad`, i.e. setting it to `NO` and directly back to `YES` – Alex Hoppen Sep 01 '15 at 08:09
2

I hit a a similar, if not the same, problem in IOS9. Changing the characteristics of some text to, say, BOLD caused the view to scroll the selection out of sight. I sorted this by adding a call to scrollRangeToVisible after the setSelectedRange:

    [self setSelectedRange:range];
    [self scrollRangeToVisible:range];
easiwriter
  • 73
  • 1
  • 8
1

Old question but I had the same issue with iOS 7 app. Requires changing the contentOffset a little bit after the run loop. Here is a quick idea.

self.clueString = [self.level clueText];
CGPoint point = self.clueText.contentOffset;
self.clueText.attributedText = self.clueString;
double delayInSeconds = 0.001; // after the run loop update
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self.clueText setContentOffset:point animated:NO]; 
});
NixonsBack
  • 390
  • 2
  • 15
1

Finally try this, checked on iOS 10

let offset = textView.contentOffset
textView.attributedText = newValue
OperationQueue.main.addOperation {
    self.textView.setContentOffset(offset, animated: false)
}
Petr Syrov
  • 14,689
  • 3
  • 20
  • 30
1

Take a look at the UITextViewDelegate, I believe the textViewDidChangeSelection method may allow you to do what you need.

drewh
  • 10,077
  • 7
  • 34
  • 43
  • Thanks for your input. What should happen in this delegate method? Is this when the final resetting of the contentOffset and selectedRange should take place? – Michael Waterfall May 14 '09 at 16:54
0

I found a solution that works reliably in iOS 6 and 7 (and probably earlier versions). In a subclass of UITextView, do the following:

@interface MyTextView ()
@property (nonatomic) BOOL needToResetScrollPosition;
@end

@implementation MyTextView

- (void)setText:(NSString *)text
{
    [super setText:text];
    self.needToResetScrollPosition = YES;
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (self.needToResetScrollPosition) {
        self.contentOffset = CGPointMake(0, 0);
        self.needToResetScrollPosition = NO;
    }
}

None of the other answers work in iOS 7 because it will adjust the scroll offsets at display time.

phatmann
  • 18,161
  • 7
  • 61
  • 51
  • self.contentOffset is always 0,0 for me (this subclass has no effect at preventing scrolling under iOS 7). – Michael Mar 11 '14 at 06:18
0

Not so elegant solution- but it works so who cares:

- (IBAction)changeTextProgrammaticaly{
     myTextView.text = @"Some text";
     [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(rewindOffset) userInfo:nil repeats:NO];
}

- (void)rewindOffset{
    [myTextView setContentOffset:CGPointMake(0,0) animated: NO];
}
phatmann
  • 18,161
  • 7
  • 61
  • 51
Tiger
  • 383
  • 1
  • 5
  • 13
  • (This solution came after all the other solutions suggested here didn't work for me) – Tiger Feb 03 '10 at 11:29
  • This does not deserve a downvote. Sometimes scrolling issues like these have to be solved in the run loop. I know there might be more elegant solutions but this works and is harmless. – phatmann Sep 21 '13 at 15:33