n.evermind
n.evermind

Reputation: 12004

Limit the number of lines for UITextview

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

Upvotes: 84

Views: 101111

Answers (11)

kwiknik
kwiknik

Reputation: 885

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
}

Upvotes: 3

Ping
Ping

Reputation: 155

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;

}

Upvotes: 1

Ben Shabat
Ben Shabat

Reputation: 387

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

Upvotes: 5

heyfrank
heyfrank

Reputation: 5647

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)
}        

Upvotes: 7

Jamagas
Jamagas

Reputation: 2677

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;

Upvotes: 266

Antzi
Antzi

Reputation: 13414

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
        }
    }
}

Upvotes: 1

standousset
standousset

Reputation: 1151

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).

Upvotes: 3

levin varghese
levin varghese

Reputation: 810

in Swift 3.0 version:

self.textView.textContainer.maximumNumberOfLines = self.textViewNumberOflines
self.textView.textContainer.lineBreakMode = .byTruncatingTail

Upvotes: 14

Maciek Czarnik
Maciek Czarnik

Reputation: 6181

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

Upvotes: 59

Jon - LBAB
Jon - LBAB

Reputation: 938

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

Upvotes: 12

Anomie
Anomie

Reputation: 94794

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.

Upvotes: 15

Related Questions