Monday, 7 January 2019

UITextView's textContainer renders at the wrong frame when constraints are changed

All I want is to resize the UITextView whenever you type/paste on it. It should be scrollable because I don't want the UITextView to fill up the screen. It could be easily achievable with these few lines of codes.

@IBOutlet weak var mainTextView: UITextView!
@IBOutlet weak var mainTextViewHeightConstraint: NSLayoutConstraint!

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

// This method is used for calculating what would be the UITextView's height
func heightOf(text: String, for textView: UITextView) -> CGFloat {
    let nsstring: NSString = text as NSString
    let boundingSize = nsstring.boundingRect(
        with: CGSize(width: textView.frame.size.width, height: .greatestFiniteMagnitude),
        options: .usesLineFragmentOrigin,
        attributes: [.font: textView.font!],
        context: nil).size
    return ceil(boundingSize.height + textView.textContainerInset.top + textView.textContainerInset.bottom)
}

// The lines inside will change the UITextView's height
func textViewDidChange(_ textView: UITextView) {
    let newString: String = textView.text ?? ""
    let minHeight = self.heightOf(text: "", for: textView) // 1 line height
    let maxHeight = self.heightOf(text: "\n\n\n\n\n", for: textView) // 6 lines height
    let contentHeight = self.heightOf(text: newString, for: textView) // height of the new string
    self.mainTextViewHeightConstraint.constant = min(max(minHeight, contentHeight), maxHeight)
}

This works perfectly well, except when the you paste a multi-line text.


To solve the pasting issue, various SO questions suggested that I either create a subclass of UITextView to override the paste(_:) method or I use the textView(_: shouldChangeTextIn....) delegate method. After a few experiments with both the best answer was to use the shouldChangeTextIn method which resulted my code into this

// I replaced `textViewDidChange(_ textView: UITextView)` with this
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    let newString: String = ((textView.text ?? "") as NSString).replacingCharacters(in: range, with: text)
    let minHeight = self.heightOf(text: "", for: textView) 
    let maxHeight = self.heightOf(text: "\n\n\n\n\n", for: textView) 
    let contentHeight = self.heightOf(text: newString, for: textView)
    self.mainTextViewHeightConstraint.constant = min(max(minHeight, contentHeight), maxHeight)
    return true
}


Now what should happen is that when the you paste text the UITextView should look like this. (The pasted string is "A\nA\nA\nA\nA" or 5 lines of just A's)

enter image description here

However it becomes like this

enter image description here

As you can see from the screenshots, the textContainer's frame is at the wrong position. Now the weird thing about this is that it seems to only happen when you're pasting at an empty UITextView.

I've tried setting and resetting the UITextView's frame.size, I've tried manually setting the NSTextContainer's size, and some other hacky solutions but I just can't seem to fix it. How can I solve this?

Note:

OS Support: iOS 8.xx - iOS 12.xx Xcode: v10.10 Swift: 3 and 4 (I tried on both)

PS: yes I have already been on other SO questions such ones listed on the bottom and a few others



from UITextView's textContainer renders at the wrong frame when constraints are changed

No comments:

Post a Comment