Nstextview autolayout


My notes on Autolayout and NSTextView / UITextview

⚠️️ This article is still WIP ⚠️️

The key to make text view height follow it’s content is by NOT SET HEIGHT CONSTRAINT and DISABLE THE SCROLL.

theTextView.isScrollEnabled = false
theTextView.text = "some text"
theTextView.sizeToFit()

But, if you already set the height constraint, then make it inactive

theTextViewHeightConstraint.isActive = false

theTextView.isScrollEnabled = false
theTextView.text = "some text"
theTextView.sizeToFit()

Set height constraint to self.contentSize.height

self.invalidateIntrinsicContentSize()

Maybe

TextViewHeightConstraint.constant = [TextView intrinsicContentSize].height
[self.myText sizeToFit];
func textViewDidChange(textView: UITextView) {

    // dynamic height adjustments

    var height = ceil(textView.contentSize.height) // ceil to avoid decimal

    if height != textViewHeight.constant { // set when height changed
        textViewHeight.constant = height
        textView.setContentOffset(CGPointZero, animated: false) // scroll to top to avoid "wrong contentOffset" artefact when line count changes
    }
}

or

All you have to do is:

set up all your constraints except the height one AND set textView’s scrollEnabled property to NO The last part is what does the trick.

Your text view will size automatically depending on it’s text value.

myTextView.textContainer.heightTracksTextView = true Which allows scroll to be enabled

it is possible. create textview of one line. attach leading,trailing,bottom to its superview and give height constraint of greater than equal to 40. this will give minimum height constraint to textivew and as and when you start typing, you need to keep checking if content goes onto second line and increase height by changing constant value of height constraint.

Here’s a subclass of UITextView I’ve come up with that allows auto expansion with auto layout but that you could still constrain to a maximum height and which will manage whether the view is scrollable depending on the height. By default the view will expand indefinitely if you have your constraints setup that way. https://stackoverflow.com/a/41770390/5389500

import UIKit

class FlexibleTextView: UITextView {
    // limit the height of expansion per intrinsicContentSize
    var maxHeight: CGFloat = 0.0
    private let placeholderTextView: UITextView = {
        let tv = UITextView()

        tv.translatesAutoresizingMaskIntoConstraints = false
        tv.backgroundColor = .clear
        tv.isScrollEnabled = false
        tv.textColor = .disabledTextColor
        tv.isUserInteractionEnabled = false
        return tv
    }()
    var placeholder: String? {
        get {
            return placeholderTextView.text
        }
        set {
            placeholderTextView.text = newValue
        }
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        isScrollEnabled = false
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: Notification.Name.UITextViewTextDidChange, object: self)
        placeholderTextView.font = font
        addSubview(placeholderTextView)

        NSLayoutConstraint.activate([
            placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
            placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
            placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
            placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var text: String! {
        didSet {
            invalidateIntrinsicContentSize()
            placeholderTextView.isHidden = !text.isEmpty
        }
    }

    override var font: UIFont? {
        didSet {
            placeholderTextView.font = font
            invalidateIntrinsicContentSize()
        }
    }

    override var contentInset: UIEdgeInsets {
        didSet {
            placeholderTextView.contentInset = contentInset
        }
    }

    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize

        if size.height == UIViewNoIntrinsicMetric {
            // force layout
            layoutManager.glyphRange(for: textContainer)
            size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
        }

        if maxHeight > 0.0 && size.height > maxHeight {
            size.height = maxHeight

            if !isScrollEnabled {
                isScrollEnabled = true
            }
        } else if isScrollEnabled {
            isScrollEnabled = false
        }

        return size
    }

    @objc private func textDidChange(_ note: Notification) {
        // needed incase isScrollEnabled is set to true which stops automatically calling invalidateIntrinsicContentSize()
        invalidateIntrinsicContentSize()
        placeholderTextView.isHidden = !text.isEmpty
    }
}

I needed a text view that would automatically grow up until a certain maximum height, then become scrollable. Michael Link’s answer worked great but I wanted to see if I could come up with something a bit simpler. Here’s what I came up with:

class AutoExpandingTextView: UITextView {

    private var heightConstraint: NSLayoutConstraint!

    var maxHeight: CGFloat = 100 {
        didSet {
            heightConstraint?.constant = maxHeight
        }
    }

    private var observer: NSObjectProtocol?

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        heightConstraint = heightAnchor.constraint(equalToConstant: maxHeight)

        observer = NotificationCenter.default.addObserver(forName: UITextView.textDidChangeNotification, object: nil, queue: .main) { [weak self] _ in
            guard let self = self else { return }
            self.heightConstraint.isActive = self.contentSize.height > self.maxHeight
            self.isScrollEnabled = self.contentSize.height > self.maxHeight
            self.invalidateIntrinsicContentSize()
        }
    }
}

Do not give a static height to UIView or to UITextView

func textviewHeight() {

        let contentSize = self.TextView.sizeThatFits(self.TextView.bounds.size)
        var frame = self.TextView.frame
        frame.size.height = contentSize.height
        self.TextView.frame = frame

        self.aspectRatioTextView = NSLayoutConstraint(item: self.TextView, attribute: .Height, relatedBy: .Equal, toItem: self.TextView, attribute: .Width, multiplier: TextView.bounds.height/TextView.bounds.width, constant: 1)
        self.TextView.addConstraint(self.aspectRatioTextView!)

        self.mainViewHeightConstraint.constant = self.mainViewHeightConstraint.constant + self.TextView.frame.height
    }

Dynamic Sizing of UITextView with Autolayout

I had been working on a project where the goal was to arrange a UITextView and some UILabel/UITextFields such that all of the elements were embedded in a UIScrollView and responded to keyboard events. The UITextView would have varying lengths of text and would need to display it without the need to scroll. And sure, there are some solutions that make use of sizeThatFits and setFrame , but I found one other way to do this that seems cleaner and leverages a UITextView’s underlying components (NSLayoutManager, NSTextContainer, NSTextStorage)and Autolayout. Implementation After ensuring that the textView.text has been set, you can do the following:

Explanation: I have the constraints defined for my UITextView in my storyboard; they are simply pinning the top, leading, and trailing edges to its UIScrollView super view, while also defining a low-priority bottom and height constraint. These two constraints (bottom & height) are both set in storyboard solely to silence warnings about ambiguous constraints. Line 2 stores the UIEdgeInsets of the UITextView, which by default are top: 8, left: 0, bottom: 8, right: 0. These values are needed to factor into the overall height. Line 3 accesses the NSLayoutManager of the UITextView in order to call usedRect(for:) which “returns the text container’s currently used area, which determines the size that the view would need to be in order to display all the glyphs that are currently laid out in the container.” usedRect(for:) is passed the textView.textContainer (NSTextContainer) property. Presumably, usedRect(for:) calls on textView.textContainer.size in order to determine its bounds. The animation on lines 9–12 were added to make the resizing feel smoother, but can be considered completely optional. And there you have it, a simple 3-line solution to dynamic UITextView height using its underlying CoreText-based components.

func updateTextViewHeight(animated: Bool) {
   let textContainterInsets = self.textView.textContainerInset
   let usedRect = self.textView.layoutManager.usedRect(for: self.textView.textContainer)

   // This is a reference to the constraint defined in storyboard
   self.textViewHeightConstraint.constant = usedRect.size.height + textContainterInsets.top + textContainterInsets.bottom

   // animate if desired
   if !animated { return }
   UIView.animate(withDuration: 0.2, animations: {
     self.view.layoutIfNeeded()
   })
 }

If you prefer to do it all by auto layout:

In Size Inspector:

Set Content Compression Resistance Priority Vertical to 1000.

Lower the priority of constraint height for your UITextView. Just make it less than 1000. Uncheck Scrolling Enabled.

Maybe solution:

public static nfloat GetEstimateHeight(this UITextView textView, UIView View)
{
    var size = new CoreGraphics.CGSize(View.Frame.Width, height: float.PositiveInfinity);
    var estimatedSize = textView.SizeThatFits(size);
    return estimatedSize.Height;
}
var size = new CoreGraphics.CGSize(View.Frame.Width, height: float.PositiveInfinity);
    var estimatedSize = textView.SizeThatFits(size);
    var textViewFinalHeight = estimatedSize.Height;

Size to fit:

theTextViewHeightConstraint.isActive = false

theTextView.isScrollEnabled = false
theTextView.text = "some text"
theTextView.sizeToFit()

Resources:

  • dealing with keyboard coming into play: https://pinkstone.co.uk/how-to-resize-a-uitextview-dynamically-with-auto-layout/
  • https://lapcatsoftware.com/articles/autoresizing-uitextview.html

Todo:

  • write about intrinsicContentSize
  • and fittingsize
  • and .contentSize.height
  • and $0.sizeToFit()
  • and let textWidth: CGFloat = text.size(withAttributes: [.font: Font.title]).width + 12 // calcs width here