user4385051
user4385051

Reputation:

Limit characters by UITextView height

Using the following code I'm attempting to limit the height of my UITextView by disallowing the user to enter characters once the UITextView is beyond a certain content size. But the problem is that with the current code, the last character after the height limit is written anyway such that one character ends up on its own line alone and that line is beyond the height limit.

How I can fix my code so that the text doesn't go past my height limit?

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

    var frame:CGRect = textView.frame;

    frame.size.height = textView.contentSize.height;

    if(frame.size.height <= 33.0){
        return true  
    }
    else {
        return false
    }
}

Upvotes: 1

Views: 1063

Answers (2)

ram
ram

Reputation: 78

Thanks @lyndsey-scott, Below is the same code updated for latest sdk in xcode 9.1. Made minor edits (replaced max height as a variable)

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    // Combine the new text with the old
    let combinedText = (textView.text as NSString).replacingCharacters(in: range, with: text)
    // Create attributed version of the text
    let attributedText = NSMutableAttributedString(string: combinedText)
    let font = textView.font ?? UIFont.systemFont(ofSize: 12.0)
    attributedText.addAttribute(NSAttributedStringKey.font, value: font, range: NSMakeRange(0, attributedText.length))
    // Get the padding of the text container
    let padding = textView.textContainer.lineFragmentPadding
    // Create a bounding rect size by subtracting the padding
    // from both sides and allowing for unlimited length
    let boundingSize = CGSize(width: textView.frame.size.width - padding * 2, height: CGFloat.greatestFiniteMagnitude)
    // Get the bounding rect of the attributed text in the
    // given frame
    let boundingRect = attributedText.boundingRect(with: boundingSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
    // Compare the boundingRect plus the top and bottom padding
    // to the text view height; if the new bounding height would be
    // less than or equal to the height limit, append the text
    if (boundingRect.size.height + padding * 2 <= MyViewController.maximumHeaderHeight){
        return true
    } else {
        return false
    }
}

Upvotes: 0

Lyndsey Scott
Lyndsey Scott

Reputation: 37300

The problem with your current code is that you're using textView.contentSize.height for comparison when your text view has yet to contain the replacement text; so as your code stands, if the text view's current content size is <= 33, it will still allow you to enter characters (i.e. return true) such that those returned characters can in fact breach the text view's height limit.

Update: I didn't really like my original answer because I thought boundingRectWithSize would provide a cleaner solution. The problem was though, that it wasn't working for me and the text would go a little past the line limit...until now. The trick is to factor in the text container's padding.

func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    // Combine the new text with the old
    let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)

    // Create attributed version of the text
    let attributedText = NSMutableAttributedString(string: combinedText)
    attributedText.addAttribute(NSFontAttributeName, value: textView.font, range: NSMakeRange(0, attributedText.length))

    // Get the padding of the text container
    let padding = textView.textContainer.lineFragmentPadding

    // Create a bounding rect size by subtracting the padding
    // from both sides and allowing for unlimited length 
    let boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFloat.max)

    // Get the bounding rect of the attributed text in the
    // given frame
    let boundingRect = attributedText.boundingRectWithSize(boundingSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)

    // Compare the boundingRect plus the top and bottom padding
    // to the text view height; if the new bounding height would be
    // less than or equal to the height limit, append the text
    if (boundingRect.size.height + padding * 2 <= 33.0){
        return true
    }
    else {
        return false
    }
}

Original Solution:

To fix this with a solution as close to your current code as possible, you could duplicate your text view, append the new text to the old, and then return true only if the updated text view inclusive of the new text has a size less than the height limit, ex:

func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    // Combine the new text with the old
    let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)

    // Create a duplicate of the text view with the same frame and font
    let duplicateTextView = UITextView(frame: textView.frame)
    duplicateTextView.font = textView.font

    // Set the text view to contain the tentative new version of the text
    duplicateTextView.text = combinedText

    // Use sizeToFit in order to make the text view's height fit the text exactly
    duplicateTextView.sizeToFit()

    // Then use the duplicate text view's height for the comparison
    if(duplicateTextView.frame.size.height <= 33.0){
        return true
    }
    else {
        return false
    }
}

Upvotes: 1

Related Questions