9

I have a UITextView which is editable. Now I have a requirement which requires me to find (as and when I keep typing) as to when the next line has begun (maybe due to me hitting the return key, or an auto wordwrap linebreak). Is there any notification which can be obtained to figure out when the next line has started while typing?

I tried searching for solutions to find out cursor positions within a textview but using selectedRange and the location properties to find it out does not help me. There is no correlation at all between the location value and a new line. The location value just keeps increasing on typing. Any ideas?

Thanks!

Bourne
  • 10,094
  • 5
  • 24
  • 51

7 Answers7

11

The following delegate is called whenever a new text is entered in textView.

Set the delegate for UITextView, then code as follows

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
{
    if ( [text isEqualToString:@"\n"] ) {
        //Do whatever you want
    }
    return YES;
}
KingofBliss
  • 15,055
  • 6
  • 50
  • 72
  • 2
    I already use UITextViewTextDidChangeNotification. And how do I look for a \n in text? Doesn't the textView.text return the entire bunch of text typed thus far? An auto linebreakwordwrap might have happened or I might have hit a lot of spaces. How do I detect the arrival of the next line in that context? – Bourne Feb 02 '11 at 12:24
  • @bourne: yes its detecting the return key alone – KingofBliss Feb 02 '11 at 12:39
  • 5
    Yes it does. But it detects a newline only when there is an empty textView and the first thing you hit inside it is a return key. I am looking to handle the multiline textView wordwrap scenario where the wordwrap automatically causes a linebreak. I wish to detect that event somehow. – Bourne Feb 02 '11 at 14:27
  • Was this ever solved? I need to detect word-wrap line breaks as well. – Albert Renshaw Sep 09 '11 at 04:09
  • 2
    I solved it just not by detecting the height of the textview (mines called hiddenText) and dividing it by the change in height with a new line... – Albert Renshaw Sep 09 '11 at 04:18
6

For Swift use this

previousRect = CGRectZero

 func textViewDidChange(textView: UITextView) {

        var pos = textView.endOfDocument
        var currentRect = textView.caretRectForPosition(pos)
        if(currentRect.origin.y > previousRect?.origin.y){
            //new line reached, write your code
        }
        previousRect = currentRect

    }

For Objective C

CGRect previousRect = CGRectZero;
- (void)textViewDidChange:(UITextView *)textView{

    UITextPosition* pos = textView.endOfDocument;
    CGRect currentRect = [textView caretRectForPosition:pos];

    if (currentRect.origin.y > previousRect.origin.y){
            //new line reached, write your code
        }
    previousRect = currentRect;

}
Sourav Gupta
  • 762
  • 11
  • 13
  • works great for swift but scrolling must be enabled on the textView – Garret Kaye Mar 03 '16 at 15:46
  • This doesn't work. If you type a bunch of text and then begin editing in the middle of the document, the origin becomes invalid and the condition is no longer true – Allison May 28 '19 at 00:46
3

Will detect line changes from anything to hitting "return", backspacing to reduce line-count, typing till the end of the line and a word-wrap occurs, etc. (*Note: MUST adjust variables for font size, I recommend not using hard coded numbers like in my example below).

previousNumberOfLines = ((hiddenText.contentSize.height-37+21)/(21));//numbers will change according to font size
NSLog(@"%i", previousNumberOfLines);
Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
  • Coming back to this many years later, I'd recommend now using UITextView's `.font.pointSize` property, instead of just using the hard coded numbers I had given in my example. Note that when typing, some complex unicode characters can cause contentSize to slightly adjust height without a new line actually occurring, so it's important you don't just monitor contentSize.height changes alone, but divide by the font pointSize so that the math accounts for the threshold of an actual new line and not just a slight line-height change. – Albert Renshaw Sep 27 '19 at 21:30
1

I'm a bit late to the party but I had a similar requirement, and after a bit of investigation I found that KingofBliss' answer combined with

-[id<NSLayoutManagerDelegate> layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex:];
-[id<NSLayoutManagerDelegate> layoutManager:shouldBreakLineByHyphenatingBeforeCharacterAtIndex:];

Did the trick for me.

You can set any object as the delegate of UITextView's layout manager like so:

textView.textContainer.layoutManager.delegate = (id<NSLayoutManagerDelegate>)delegate

Hope this will prover useful.

Community
  • 1
  • 1
Gabor
  • 278
  • 4
  • 14
1

Sourav Gupta's solution partially worked for me however it had a few pitfalls, namely that it didn't correctly call the target code if the editing happened in the middle of the text body (for example: writing a bunch of text, moving the cursor to the middle and then hitting return three times) as well as handling insertion events like pasting and text correction. As a result, I did some testing on my own and found a few more conditions in which we can assume that editing has moved to the next line.

note: this approach has a few false positives but I found that to be more tolerable as I use this for relaying out the code and so laying out unnecessarily is far better than not responding

Swift 5


private var previousRect:CGRect = CGRect.zero
private func checkIfReturnOrLineWrap(textView:UITextView) {
    let currentRect = textView.caretRect(for:textView.endOfDocument)
    if (currentRect.origin.y != previousRect.origin.y && (previousRect.origin.y != CGFloat.infinity || currentRect.origin.y > 0) ||
        currentRect.origin.y == CGFloat.infinity) {
        //React to the line change!
    }
    previousRect = currentRect
}

//MARK: UITextViewDelegate methods (be sure to hook up your textviews!)
func textViewDidChange(_ textView: UITextView) {
    checkIfReturnOrLineWrap(textView: textView)
}

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    checkIfReturnOrLineWrap(textView: textView)
    return true
}
Community
  • 1
  • 1
Allison
  • 2,213
  • 4
  • 32
  • 56
0

Add a second, hidden text view to your view. Implement shouldChangeTextInRange for the visible text view, and set the text on the hidden view to the new text. Compare the contentSize for the new vs. old text to detect word wrap.

Phil Loden
  • 1,394
  • 1
  • 15
  • 15
0
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
 replacementText:(NSString *)text
{

    if ([text isEqualToString:@"\n"]) {
        textView.text=[NSString stringWithFormat:@"%@\n",textView.text];
        // Return FALSE so that the final '\n' character doesn't get added
        return NO;
    }
    // For any other character return TRUE so that the text gets added to the view
    return YES;
}
KingofBliss
  • 15,055
  • 6
  • 50
  • 72
Hardik Mamtora
  • 1,642
  • 17
  • 23