82

I was wondering how to limit the amount of LINES (not characters as asked in other questions) a user can enter when editing a UITextField.

Ideally, I would like to limit the input to max. 10 lines.

Where would I need to start? Do I do this with a method? In

 - (BOOL)textViewShouldBeginEditing:(UITextView *)aTextView
BenMorel
  • 34,448
  • 50
  • 182
  • 322
n.evermind
  • 11,944
  • 19
  • 78
  • 122
  • possible duplicate of [Limiting text in a UITextView](http://stackoverflow.com/questions/411398/limiting-text-in-a-uitextview) – santhu Jan 25 '14 at 15:54
  • You can find your answer [in this post](http://stackoverflow.com/questions/411398/limiting-text-in-a-uitextview). – Zakaria Mar 07 '11 at 22:27

11 Answers11

260

Maciek Czarnik answer does not worked for me, but it got me insights what to do.

iOS 7+

Swift

textView.textContainer.maximumNumberOfLines = 10
textView.textContainer.lineBreakMode = .byTruncatingTail

ObjC

textView.textContainer.maximumNumberOfLines = 10;
textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
Jamagas
  • 2,617
  • 2
  • 10
  • 7
59

Maybe this can help (iOS 7+):

textView.textContainer.maximumNumberOfLines = 10;
[textView.layoutManager textContainerChangedGeometry:textView.textContainer];

Even first line should do the trick I guess, but doesn't... Maybe its a bug in SDK

Maciek Czarnik
  • 5,950
  • 2
  • 37
  • 50
15

You have the right idea, but the wrong method. textView:shouldChangeTextInRange:replacementText: is called whenever the text is going to change; you can access the current content of the text view using its text property, and you can construct the new content from the passed range and replacement text with [textView.text stringByReplacingCharactersInRange:range withString:replacementText]. You can then count the number of lines and return YES to allow the change or NO to reject it.

Anomie
  • 92,546
  • 13
  • 126
  • 145
  • Or did you mean on-screen lines, rather than logical lines of text? – Anomie Mar 08 '11 at 03:37
  • Actually, yes, I want to know on-screen lines. I want to load a txt file into a UITextView and limit the lines shown on the screen to 10 or 15 and then have the rest on the next screen. It's really like a little word processor which has individual pages. That's why I want to limit the lines (on screen) displayed in the UITextView. But perhaps there is an easier way to do this? Perhaps use UIWebView? – n.evermind Mar 08 '11 at 09:12
  • 1
    If all you want is paged behavior, note that `UITextView` is a subclass of `UIScrollView` and so has the `pagingEnabled` property. – Anomie Mar 08 '11 at 11:47
  • I don't think this works as shouldChangeTextInRange can only count the number of lines prior to the new input. Adding a line for each time the new text is "\n" doesn't work either as the user could naturally reach the end of the line. – Declan McKenna Jun 18 '13 at 14:33
14

in Swift 3.0 version:

self.textView.textContainer.maximumNumberOfLines = self.textViewNumberOflines
self.textView.textContainer.lineBreakMode = .byTruncatingTail
currarpickt
  • 2,290
  • 4
  • 24
  • 39
levin varghese
  • 810
  • 12
  • 12
12

Maciek Czarnik's answer does not seem to work for me, even in iOS7. It gives me strange behavior, I don't know why.

What I do to limit the number of lines in the UITextView is simply :

(tested only in iOS7) In the following UITextViewDelegate method :

- (void)textViewDidChange:(UITextView *)textView
{
    NSUInteger maxNumberOfLines = 5;
    NSUInteger numLines = textView.contentSize.height/textView.font.lineHeight;
    if (numLines > maxNumberOfLines)
    {
        textView.text = [textView.text substringToIndex:textView.text.length - 1];
    }
}
Jon - LBAB
  • 928
  • 11
  • 20
7

Here's a improved Version of Numereyes answer in Swift 4.2 / Swift 5

I made a little extension so I can reuse the code. I'm using a While-Loop to check if the size fits. This also works when the user pastes a lot of text at once.

extension UITextView {        
    var numberOfCurrentlyDisplayedLines: Int {
        let size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        //for Swift <=4.0, replace with next line:
        //let size = systemLayoutSizeFitting(UILayoutFittingCompressedSize)

        return Int(((size.height - layoutMargins.top - layoutMargins.bottom) / font!.lineHeight))
    }

    /// Removes last characters until the given max. number of lines is reached
    func removeTextUntilSatisfying(maxNumberOfLines: Int) {
        while numberOfCurrentlyDisplayedLines > (maxNumberOfLines) {
            text = String(text.dropLast())
            layoutIfNeeded()
        }
    }
}

// Use it in UITextView's delegate method:
func textViewDidChange(_ textView: UITextView) {        
    textView.removeTextUntilSatisfying(maxNumberOfLines: 10)
}        
heyfrank
  • 5,291
  • 3
  • 32
  • 46
  • This seems to rely on newlines present in the text, it doesn't work if there aren't any, always reports 0 lines even when there are multiple visible / displayed lines. – kwiknik Feb 20 '23 at 19:19
  • @kwiknik i think you wanted to comment this answer: https://stackoverflow.com/a/54924572/4846592 ? – heyfrank Feb 21 '23 at 10:19
  • I can see why you're confused, let me explain better: when I tried your solution and the text contained newlines, it worked. But when the text contained no newlines yet was still visibly wrapped into multiple lines, the code you've given didn't calculate the number of visible lines, it returned zero. – kwiknik Feb 21 '23 at 12:13
5

Swift 4

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
    let newLines = text.components(separatedBy: CharacterSet.newlines)
    let linesAfterChange = existingLines.count + newLines.count - 1
    return linesAfterChange <= textView.textContainer.maximumNumberOfLines
}

And if you want to limit characters also:

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
        let newLines = text.components(separatedBy: CharacterSet.newlines)
        let linesAfterChange = existingLines.count + newLines.count - 1
        if(text == "\n") {
            return linesAfterChange <= textView.textContainer.maximumNumberOfLines
        }

        let newText = (textView.text as NSString).replacingCharacters(in: range, with: text)
        let numberOfChars = newText.count
        return numberOfChars <= 30 // 30 characters limit
    }
}

don't forget to add how many lines you want the limit to be in viewDidLoad:

txtView.textContainer.maximumNumberOfLines = 2
Ben Shabat
  • 388
  • 4
  • 17
3

The other solutions given do not solve an issue related to a last line being created at the end (an 11th line in the question's case).

Here is a working solution with Swift 4.0 & Xcode 9.0 beta (found on this blog post)

   class ViewController: UIViewController, UITextViewDelegate {

      @IBOutlet weak var textView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
        textView.textContainer.maximumNumberOfLines = 10
        textView.textContainer.lineBreakMode = .byWordWrapping
      }

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

        let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
        let newLines = text.components(separatedBy: CharacterSet.newlines)
        let linesAfterChange = existingLines.count + newLines.count - 1

        return linesAfterChange <= textView.textContainer.maximumNumberOfLines
    }

Nota bene: This solution does not handle the scenario where the last line is too long to display (text will be hidden on the far right side of the UITextView).

standousset
  • 1,092
  • 1
  • 10
  • 25
1

Similar to other answers, but usable directly from Storyboard and without subclassing:

extension UITextView {
    @IBInspectable var maxNumberOfLines: NSInteger {
        set {
            textContainer.maximumNumberOfLines = maxNumberOfLines
        }
        get {
            return textContainer.maximumNumberOfLines
        }
    }
    @IBInspectable var lineBreakByTruncatingTail: Bool {
        set {
            if lineBreakByTruncatingTail {
                textContainer.lineBreakMode = .byTruncatingTail
            }
        }
        get {
            return textContainer.lineBreakMode == .byTruncatingTail
        }
    }
}
Antzi
  • 12,831
  • 7
  • 48
  • 74
0

Refer to APPLE documentation: Counting Lines of Text https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html

    - (void)textViewDidChangeSelection:(UITextView *)textView{
NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
(unsigned)[layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
    (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
            effectiveRange:&lineRange];
    index = (unsigned)NSMaxRange(lineRange);
}
if(numberOfLines > 10){
    self.textField.text = self.lastString;
}

}

handle "\n"

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
if([text containsString:@"\n"]){
    NSLayoutManager *layoutManager = [textView layoutManager];
    unsigned numberOfLines, index, numberOfGlyphs =
            (unsigned)[layoutManager numberOfGlyphs];
    NSRange lineRange;
    for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
        (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
                effectiveRange:&lineRange];
        index = (unsigned)NSMaxRange(lineRange);
    }
    NSLog(@"lines = %d",numberOfLines);
    if(numberOfLines >= 10){
        return NO;
    }
}
self.lastString = textView.text;

return YES;

}

Ping
  • 145
  • 2
  • 6
0

Swift 5.7, iOS 16

Inside your UIViewRepresentable wrapper for UITextView (or UITextField)

func makeUIView(context: Context) -> UITextView {

    let textView = UITextView() // ..or MyCustomUITextView()

    // ..your existing makeUIView body here..

    // Limit to N = 3 visible (displayed) lines.
    textView.textContainer.maximumNumberOfLines = 3

    // When visible line count is exceeded typing can continue,
    // but what's entered will be truncated so hidden from view.
    textView.textContainer.lineBreakMode = .byTruncatingTail

    // ..or prevent further text entry when limit is hit.
    // See https://developer.apple.com/documentation/uikit/nslinebreakmode
    //textView.textContainer.lineBreakMode = .byClipping

    // Set delegate now that other changes are made.
    textView.delegate = self

    return textView
}

kwiknik
  • 570
  • 3
  • 7