Edited at

A content-wrapping, scrollable multiline UITextView with maximum size in Swift


Conditions

Assume the following, common requirements:


  • A UITextView with text appears at center of screen

  • The textView height must adjust to fit the text height, but is scrollable if beyond a specified maximum height

  • The textView width must adjust to fit the text width, up to a maximum width

  • The textView must use center justified text if only one line, natural justified text other otherwise


Solution


  • Initialize the UITextView with the following properties

textView.translatesAutoresizingMaskIntoConstraints = false

textView.isScrollEnabled = true
textView.showsVerticalScrollIndicator = true
textView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)


  • Set the textView properties to multiline:

textView.numberOfLines = 0

textView.lineBreakMode = .byWordWrapping


  • Set vertical content compression on the textView, with a value which is less than the default 1000:

textView.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 500), for: UILayoutConstraintAxis.vertical)


  • Put the textView inside a custom container view, and constrain their edges to be equal


  • Set fixed constraints on the container width and height, so that they are no more than some fraction of a parent view size. These constraints will adopt the default priority of 1000, and are therefore prioritized ahead of constraints set by the textView content.


    NSLayoutConstraint(item: containerView,

attribute: .height,
relatedBy: .lessThanOrEqual,
toItem: parentView,
attribute: .height,
multiplier: containerSizeFraction, // e.g. 0.7
constant: 0.0),


  • Override "layoutSubviews" of the container view to perform the remainder of the fitting - it will be called when adding the the UITextView to the container view. We summarize the fitting with the following lines:

First, ensure the textView is always showing the top line when its layout is rearranged.


override func layoutSubviews() {
super.layoutSubviews()

textView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false)

Then compute the textView size based its content

    let size = textView.sizeThatFits(CGSize(width: textView.contentSize.width, height: CGFloat.greatestFiniteMagnitude))

Constrain the textView width and height to size.width and size.height respectively. For each constraint, set the priority as:

    widthConstraint.priority = UILayoutPriority(rawValue: 750) }

heightConstraint.priority = UILayoutPriority(rawValue: 750) }

Set the text alignment according to the current number of lines.

    textView.textAlignment = ( textView.contentSize.height >= font.lineHeight) ? .natural : .center

textView.setNeedsDisplay()

It is helpful to briefly show the scroll indicator to highlight if the textView is now scrollable

    textView.flashScrollIndicators()