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 the string's height in a UITextField
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)
}
// This method calculates the UITextView's new height
func calculateHeightFor(text: String, in textView: UITextView) -> CGFloat {
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
return min(max(minHeight, contentHeight), maxHeight)
}
// The lines inside will change the UITextView's height
func textViewDidChange(_ textView: UITextView) {
self.mainTextViewHeightConstraint.constant = calculateHeightFor(
text: textView.text ?? "",
in: textView)
}
This works perfectly well, except when the you paste a multi-line text, or when you press enter (line break).
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 {
self.mainTextViewHeightConstraint.constant = calculateHeightFor(
text: ((textView.text ?? "") as NSString).replacingCharacters(in: range, with: text),
in: textView)
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)
However it becomes like this
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?
Additional Information:
Upon looking at the view debugger, here's how it looks like when you type enter (line break)
As we can see, the text container is at the wrong location.
After all this adding a UIView.animate
block seems to fix some of the issues but not all of it. Why does this work?
// These snippet kind of fixes the problem
self.mainTextViewHeightConstraint.constant = calculateHeightFor(
text: textView.text ?? "",
in: textView)
UIView.animate(withDuration: 0.1) {
self.view.layoutIfNeeded()
}
Note: I didn't place the UIView.animate
solution because it doesn't cover all the bugs.
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