iosuitextviewnstextcontainer

Prevent UITextView from offsetting its text container


I am tying to modify the height of a UITextView dynamically (up to a max height) while the user enters text. I am experiencing a very strange behavior when there are an even number of lines in the text view.

I am using autolayout and the text view has a height constraint. I respond to calls to the text view's delegate (textViewDidChange(_:)), where I calculate and adjust the height constraint based on the contentSize.

Here is the code:

func textViewDidChange(_ textView: UITextView) {
    let newHeight = textView.contentSize.height
    let newConstraintConst = max(MinTextViewHeight, min(MaxTextViewHeight, newHeight))
    self.textViewHeightConstraint.constant = newConstraintConst
}

This works well, it resizes the frame up to MaxTextViewHeight and then the text view can scroll. However, when there are an even number of lines in the text view, the text view adds a kind of offset to the bottom of its NSTextContainer, causing the top line to be cut off:

top line is cut off

text container is offset

However, when there are odd lines the NSTextContainer is no longer offset:

top line is normal

text container is normal

At first I thought it was somehow being controlled by the text view's textContainerInset but that is only used to pad the space inside the NSTextContainer, as setting it to .zero removes the space inside but does not affect the offset (and incidentally makes it even worse, as the top line almost completely disappears):

removing the inset makes it even worse

I have looked through the UITextView class reference and I don't see any property that would let me manipulate or even get the value of this offset.

As a workaround I am increasing the text container's top inset and removing the bottom inset:

textView.textContainerInset = UIEdgeInsetsMake(10, 0, 0, 0)

enter image description here

This works so far, but I arrived at a value of 10 by trial-and-error, and so far I've only tested it on a single device.

I am not interested in more hacky workarounds that require fragile, fixed values; I am trying to understand how this offset is being set and a proper way to fix it. I'm hoping that someone can provide some insight, thanks!


Solution

  • Just a speculation, but I think the problem is that the text view assumes that the height of itself does not change while calling textViewDidChange, so it scrolls when it thinks it has to, regardless of you changing its frame.

    Not sure if you think my solution is too hacky, but this will stop it from scrolling when you don't want it. I simply pin the content offset to the top as long as the wanted content size is smaller than your max size.

    Just add this:

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if textView.contentSize.height <= MaxTextViewHeight && textView.contentOffset.y > 0.0 {
            textView.contentOffset.y = 0.0;
        }
    
    }