Introduction
A UITextView that grows to fit its content is a common requirement in iOS apps — chat bubbles, comment fields, and form inputs all need this behavior. The key is disabling scrolling (isScrollEnabled = false), which tells the text view to use its intrinsic content size instead of a fixed scrollable frame. Combined with Auto Layout constraints that do not fix the height, the text view automatically expands as the user types. For dynamic height in table or collection view cells, you also need to invalidate the cell layout when the text changes.
Basic Self-Sizing UITextView
1class ViewController: UIViewController {
2 let textView = UITextView()
3
4 override func viewDidLoad() {
5 super.viewDidLoad()
6
7 textView.translatesAutoresizingMaskIntoConstraints = false
8 textView.isScrollEnabled = false // Critical: enables intrinsic content size
9 textView.font = UIFont.systemFont(ofSize: 16)
10 textView.layer.borderColor = UIColor.systemGray4.cgColor
11 textView.layer.borderWidth = 1
12 textView.layer.cornerRadius = 8
13 view.addSubview(textView)
14
15 NSLayoutConstraint.activate([
16 textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
17 textView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
18 textView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
19 // No height constraint — the text view sizes itself
20 ])
21 }
22}
When isScrollEnabled is false, UITextView reports an intrinsic content size equal to its text content. Auto Layout uses this to determine the height automatically.
Setting a Maximum Height
To prevent the text view from growing beyond a maximum height, add a height constraint and re-enable scrolling when the content exceeds it:
1class ExpandingTextView: UITextView {
2 var maxHeight: CGFloat = 200
3
4 override var intrinsicContentSize: CGSize {
5 let size = super.intrinsicContentSize
6 if size.height > maxHeight {
7 isScrollEnabled = true
8 return CGSize(width: size.width, height: maxHeight)
9 } else {
10 isScrollEnabled = false
11 return size
12 }
13 }
14
15 override var text: String! {
16 didSet { invalidateIntrinsicContentSize() }
17 }
18}
19
20// Usage
21let textView = ExpandingTextView()
22textView.maxHeight = 150
23textView.font = UIFont.systemFont(ofSize: 16)
24textView.translatesAutoresizingMaskIntoConstraints = false
When text exceeds maxHeight, scrolling is enabled and the text view stops growing. When text shrinks below the threshold, it resizes back down.
Using UITextViewDelegate for Dynamic Updates
1class ViewController: UIViewController, UITextViewDelegate {
2 let textView = UITextView()
3 var heightConstraint: NSLayoutConstraint!
4 let maxHeight: CGFloat = 200
5
6 override func viewDidLoad() {
7 super.viewDidLoad()
8 textView.delegate = self
9 textView.isScrollEnabled = false
10 textView.font = UIFont.systemFont(ofSize: 16)
11 textView.translatesAutoresizingMaskIntoConstraints = false
12 view.addSubview(textView)
13
14 heightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)
15
16 NSLayoutConstraint.activate([
17 textView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
18 textView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
19 textView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8),
20 heightConstraint
21 ])
22 }
23
24 func textViewDidChange(_ textView: UITextView) {
25 let size = textView.sizeThatFits(CGSize(
26 width: textView.frame.width,
27 height: .greatestFiniteMagnitude
28 ))
29
30 if size.height >= maxHeight {
31 textView.isScrollEnabled = true
32 heightConstraint.constant = maxHeight
33 } else {
34 textView.isScrollEnabled = false
35 heightConstraint.constant = size.height
36 }
37 }
38}
Self-Sizing Table View Cells
For expanding text views inside UITableViewCell:
1class TextViewCell: UITableViewCell {
2 let textView = UITextView()
3 weak var tableView: UITableView?
4
5 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
6 super.init(style: style, reuseIdentifier: reuseIdentifier)
7
8 textView.isScrollEnabled = false
9 textView.font = UIFont.systemFont(ofSize: 16)
10 textView.translatesAutoresizingMaskIntoConstraints = false
11 contentView.addSubview(textView)
12
13 NSLayoutConstraint.activate([
14 textView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
15 textView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
16 textView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
17 textView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
18 ])
19 }
20
21 required init?(coder: NSCoder) { fatalError() }
22}
23
24// In UITableViewDelegate
25func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
26 let cell = tableView.dequeueReusableCell(withIdentifier: "TextViewCell", for: indexPath) as! TextViewCell
27 cell.tableView = tableView
28 cell.textView.text = data[indexPath.row]
29 return cell
30}
Set tableView.rowHeight = UITableView.automaticDimension and tableView.estimatedRowHeight = 60 for automatic cell sizing.
SwiftUI Equivalent
In SwiftUI, TextEditor automatically sizes with the .fixedSize(horizontal:vertical:) modifier:
1struct ExpandingTextEditor: View {
2 @State private var text = ""
3
4 var body: some View {
5 TextEditor(text: $text)
6 .frame(minHeight: 40, maxHeight: 200)
7 .fixedSize(horizontal: false, vertical: true)
8 .padding(4)
9 .overlay(
10 RoundedRectangle(cornerRadius: 8)
11 .stroke(Color.gray.opacity(0.3))
12 )
13 }
14}
In iOS 16+, TextField also supports multi-line input with the axis: .vertical parameter:
TextField("Enter text...", text: $text, axis: .vertical)
.lineLimit(1...5)
Common Pitfalls
Forgetting to set isScrollEnabled = false: This is the single most important step. When scrolling is enabled (the default), UITextView uses a fixed frame and scrolls its content instead of expanding. The intrinsic content size is not reported to Auto Layout.
Adding a fixed height constraint: A fixed-height constraint (heightAnchor.constraint(equalToConstant: 100)) prevents the text view from expanding. Use greaterThanOrEqualToConstant for a minimum height, or omit the height constraint entirely.
Not calling invalidateIntrinsicContentSize() in custom subclasses: When you override properties like text or attributedText in a UITextView subclass, call invalidateIntrinsicContentSize() to trigger Auto Layout to recalculate the height.
Table view cells not resizing dynamically: When text changes in a cell, the table view does not automatically recalculate row heights. Call tableView.beginUpdates() / tableView.endUpdates() in textViewDidChange(_:) to trigger a height recalculation without reloading the cell.
Text view jumping when toggling isScrollEnabled: Switching isScrollEnabled between true and false can cause the text view to scroll to the top. Set contentOffset = .zero or preserve the scroll position when toggling to avoid a visual jump.
Summary
Set isScrollEnabled = false to make UITextView report its intrinsic content size to Auto Layout
Do not add a fixed height constraint — use greaterThanOrEqualToConstant for a minimum height or omit height entirely
For a maximum height, switch isScrollEnabled back to true and cap the height when content exceeds the limit
In table view cells, use UITableView.automaticDimension for row height and call beginUpdates()/endUpdates() when text changes
In SwiftUI, use TextEditor with .fixedSize(horizontal: false, vertical: true) or TextField with axis: .vertical