A problem I needed to solve was: while editing in a text view, I want the view to grow and shrink depending on the text entered. I also want this to work in conjunction with auto-layout.
This is something I’ve been fighting for some time, but now I sat down and spent enough time (and quite a bit of Stack Overflow reading) to get it to work. The first problem I kept having was finding all the parameters that determine the actual size of the text area so I could calculate it reliably. The other important problem was that most everything I tried made the text scroll up a little bit every now and then, making for an ugly jumping up and down of the whole text content. The solution I found by trial and error (mostly error) was to calculate and reset the height constraint before the text view actually set the new text, so it has to be done in shouldChangeTextInRange delegate method.
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var heightConstraint: NSLayoutConstraint!
@IBOutlet weak var myTextView: UITextView!
var lastCalculatedTextHeight: CGFloat = 0.0
override func viewDidLoad() {
super.viewDidLoad()
self.resizeTextView(myTextView)
}
func resizeTextView(textView: UITextView) {
resizeTextView(textView, forText: textView.text)
}
func resizeTextView(textView: UITextView, forText newText: String) {
if let font = textView.font {
let padding = textView.textContainer.lineFragmentPadding
let width = textView.contentSize.width – 2 * padding
let attributes = [NSFontAttributeName: font]
let newRect = newText.boundingRectWithSize(
CGSize(width: width, height: CGFloat.max),
options: [.UsesLineFragmentOrigin, .UsesFontLeading],
attributes: attributes, context: nil)
let textInset = textView.textContainerInset
let newHeight = newRect.height + textInset.bottom + textInset.top
if newHeight != lastCalculatedTextHeight {
lastCalculatedTextHeight = newHeight
UIView.animateWithDuration(0.5, animations: {
self.heightConstraint.constant = newHeight
self.view.layoutIfNeeded()
})
}
}
}
// by doing this, by capturing the new size of the textView *before* the
// text contents actually change, we can avoid having
// the scroll view jump. We resize it before it sees it needs to grow.
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange,
replacementText text: String) -> Bool {
let oldText = textView.text
if let nsRange = oldText.rangeFromNSRange(range) {
let newText = oldText.stringByReplacingCharactersInRange(nsRange,
withString: text)
resizeTextView(textView, forText: newText)
}
return true
}
}
import Foundation
extension String {
func rangeFromNSRange(nsRange: NSRange) -> Range<String.Index>? {
let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
if let from = String.Index(from16, within: self),
to = String.Index(to16, within: self) {
return from ..< to
}
return nil
}
func NSRangeFromRange(range: Range<String.Index>) -> NSRange {
let from = String.UTF16View.Index(range.startIndex, within: utf16)
let to = String.UTF16View.Index(range.endIndex, within: utf16)
return NSMakeRange(utf16.startIndex.distanceTo(from), from.distanceTo(to))
}
}
All of this is for Swift 2.x, by the way. Ah, another little neat thing: you put a height constraint on the text view through IB, then connect that constraint to an outlet. That’s the “heightConstraint” outlet at the top of the listing. (I never even thought of that before.)
You can find the whole project in a little zip-file.
Next little project is putting this to work in table cells, or maybe stack containers.