Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to limit the number of lines in UITextView?

I have a UITextView in my screen, the user should fill only two lines, and when the user in the second line the return key should turn into Done.

How can I limit the number of lines in UITextView? I searched a lot, and no results was useful!

I found this answer for Swift:

locationNoteTextView.textContainer.maximumNumberOfLines = 2
self.locationNoteTextView.textContainer.lineBreakMode = NSLineBreakMode.ByClipping

It didn't work, by this way the user can enter infinity character, but what viewed on the screen are just two lines!

So if you tried to print the text of textView you will find a disaster text.

like image 787
Rawan Avatar asked Oct 01 '15 13:10

Rawan


4 Answers

For iOS 7+

textView.textContainer.maximumNumberOfLines = 10;
textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;

OR

textView.textContainer.maximumNumberOfLines = 10;
[textView.layoutManager textContainerChangedGeometry:textView.textContainer];
like image 131
Rahul Shirphule Avatar answered Nov 16 '22 17:11

Rahul Shirphule


You need to implement textView:shouldChangeTextInRange:replacementText:. This method is called whenever the text is going to change. You can access the current content of the text view using its text property.

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.

EDIT: On OP request:

Swift:

func sizeOfString (string: String, constrainedToWidth width: Double, font: UIFont) -> CGSize {
    return (string as NSString).boundingRectWithSize(CGSize(width: width, height: DBL_MAX),
        options: NSStringDrawingOptions.UsesLineFragmentOrigin,
        attributes: [NSFontAttributeName: font],
        context: nil).size
}


func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    let newText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
    var textWidth = CGRectGetWidth(UIEdgeInsetsInsetRect(textView.frame, textView.textContainerInset))
    textWidth -= 2.0 * textView.textContainer.lineFragmentPadding;

    let boundingRect = sizeOfString(newText, constrainedToWidth: Double(textWidth), font: textView.font!)
    let numberOfLines = boundingRect.height / textView.font!.lineHeight;

    return numberOfLines <= 2;
}

Obj-C

- (CGSize) sizeOfString:(NSString*)str constrainedToWidth:(CGFloat)width andFont:(UIFont*)font
{
    return [str boundingRectWithSize:CGSizeMake(width, DBL_MAX)
                              options:NSStringDrawingUsesLineFragmentOrigin
                           attributes:@{NSFontAttributeName: font}
                              context:nil].size;
}

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
    CGFloat textWidth = CGRectGetWidth(UIEdgeInsetsInsetRect(textView.frame, textView.textContainerInset));
    textWidth -= 2.0 * textView.textContainer.lineFragmentPadding;
    CGSize boundingRect = [self sizeOfString:newText constrainedToWidth:textWidth andFont:textView.font];
    int numberOfLines = boundingRect.height / textView.font.lineHeight;
    return numberOfLines <= 2;
}
like image 45
Abhinav Avatar answered Nov 16 '22 15:11

Abhinav


In case anyone wants realtime line number count, this is how you do it with RxSwift & RxCocoa

    let disposeBag = DisposeBag()

    textView.rx_text.asDriver().driveNext { text in

        let text = text as NSString

        var textWidth: CGFloat = CGRectGetWidth(UIEdgeInsetsInsetRect(self.textView.frame, self.textView.textContainerInset))

        textWidth -= 2.0 * self.textView.textContainer.lineFragmentPadding

        let textAttributes = [NSFontAttributeName: UIFont(name: “Set your font here”, size: 26.0)!]

        let boundingRect: CGRect = text.boundingRectWithSize(CGSizeMake(textWidth, 0), options: [NSStringDrawingOptions.UsesLineFragmentOrigin, NSStringDrawingOptions.UsesFontLeading], attributes: textAttributes, context: nil)

        let font = UIFont(name: “Set your font here”, size: 26.0)

        guard let lineHeight = font?.lineHeight else {return}

        let numberOfLines = CGRectGetHeight(boundingRect) / lineHeight

        if numberOfLines <= 2 {


            self.textView.typingAttributes = [NSFontAttributeName: UIFont(name: “Set your font here”, size: 26.0)!, NSForegroundColorAttributeName: “Set your color here”]

        } else {

            self.textView.typingAttributes = [NSFontAttributeName: UIFont(name: “Set your font here”, size: 18.0)!, NSForegroundColorAttributeName: “Set your color here”]

        }

    }.addDisposableTo(disposeBag)
like image 1
Deaconu Dan Andrei Avatar answered Nov 16 '22 15:11

Deaconu Dan Andrei


I updated @Abhinav code to Swift 5, also added support to limit char count.

import UIKit

class ViewController: UIViewController {
    @IBOutlet private var textView: UITextView!

    private let maxNumberOfLines = 4
    private let maxCharactersCount = 100

    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
    }
}

extension ViewController: UITextViewDelegate {
    private func textView(_ textView: UITextView, newText: String, underLineLimit: Int) -> Bool {
        guard let textViewFont = textView.font else {
            return false
        }

        let rect = textView.frame.inset(by: textView.textContainerInset)
        let textWidth = rect.width - 2.0 * textView.textContainer.lineFragmentPadding

        let boundingRect = newText.size(constrainedToWidth: textWidth,
                                        font: textViewFont)

        let numberOfLines = boundingRect.height / textViewFont.lineHeight

        let underLimit = Int(numberOfLines) <= underLineLimit
        return underLimit
    }

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

        let newText = textView.text.replacingCharacters(in: range, with: text)

        let underLineLimit = self.textView(textView, newText: newText, underLineLimit: maxNumberOfLines)
        let underCharacterLimit = newText.count <= maxCharactersCount
        return underLineLimit && underCharacterLimit
    }
}

extension String {
    func replacingCharacters(in range: NSRange, with string: String) -> String {
        let currentNSString = self as NSString
        return currentNSString.replacingCharacters(in: range, with: string)
    }

    func size(constrainedToWidth width: CGFloat, font: UIFont) -> CGSize {
        let nsString = self as NSString
        let boundingSize = CGSize(width: width, height: .greatestFiniteMagnitude)
        return nsString
            .boundingRect(with: boundingSize,
                          options: .usesLineFragmentOrigin,
                          attributes: [.font: font],
                          context: nil)
            .size
    }
}
like image 1
Shai Balassiano Avatar answered Nov 16 '22 17:11

Shai Balassiano