numberOfLines = 0 and lineBreakMode = .byWordWrapping. When using Auto Layout, pin the label’s top, leading, and trailing edges, leaving the height unconstrained so the label’s intrinsic content size drives the height automatically.Why this happens
A UILabel defaults to a single line of text (numberOfLines = 1). When the text assigned to the label exceeds its available width, UIKit truncates the string with an ellipsis instead of wrapping it onto new lines.
Even after setting numberOfLines = 0, a label will not expand vertically if you define an explicit height constraint in Auto Layout or if you set a fixed CGRect frame without recalculating its dimensions. In Auto Layout, UIKit calculates a label’s geometry using its intrinsicContentSize. For multiline text, this intrinsic size depends directly on the available width. If you constrain both width and height, or fail to constrain the width while leaving height flexible, the layout engine cannot determine how many lines the text requires.
In older codebases, developers often called legacy Objective-C methods like sizeWithFont:constrainedToSize:lineBreakMode:. In modern Swift, UIKit provides declarative layout constraints, automatic dimension calculation, and string bounding methods that handle dynamic font sizing and localization automatically.
How to fix it
1. Use Auto Layout with Intrinsic Content Size (Recommended)
The cleanest and most robust approach in modern iOS development is to rely on Auto Layout. Pin the label’s horizontal edges (leading and trailing) and its top edge to its superview or adjacent views, and omit any height constraint. This allows the label’s intrinsicContentSize to stretch the height as needed.
import UIKit
final class TextDisplayViewController: UIViewController {
private let contentLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.font = UIFont.preferredFont(forTextStyle: .body)
label.textColor = .label
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupLayout()
populateContent()
}
private func setupLayout() {
view.addSubview(contentLabel)
NSLayoutConstraint.activate([
contentLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
contentLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
contentLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
// Note: Do not add a height constraint here.
])
}
private func populateContent() {
contentLabel.text = "Since the alien army vastly outnumbers the team, players must use the post-apocalyptic world to their advantage, such as seeking cover behind dumpsters, pillars, cars, rubble, and other objects."
}
}
If you embed the label inside a custom view where the width is not immediately determined during the initial layout pass, explicitly update preferredMaxLayoutWidth inside layoutSubviews():
override func layoutSubviews() {
super.layoutSubviews()
contentLabel.preferredMaxLayoutWidth = contentLabel.bounds.width
super.layoutSubviews()
}
2. Adjust Frame Manually Using sizeToFit()
If your project relies on manual frame-based layout rather than Auto Layout, set the target width first, configure multiline properties, and call sizeToFit(). This recalculates the view’s frame bounds based on its content while keeping its origin fixed.
import UIKit
func configureManualFrameLabel(in containerView: UIView) -> UILabel {
let padding: CGFloat = 16.0
let targetWidth = containerView.bounds.width - (padding * 2)
let label = UILabel(frame: CGRect(x: padding, y: padding, width: targetWidth, height: 0))
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.font = UIFont.systemFont(ofSize: 16)
label.text = "Since the alien army vastly outnumbers the team, players must use the post-apocalyptic world to their advantage, such as seeking cover behind dumpsters, pillars, cars, rubble, and other objects."
// sizeToFit resizes both width and height to fit the text.
// Reset the width afterwards if you need the label to retain full container width.
label.sizeToFit()
label.frame.size.width = targetWidth
return label
}
Alternatively, use sizeThatFits(_:) if you want to inspect or apply the calculated CGSize without modifying the view’s frame immediately:
let constrainedSize = CGSize(width: targetWidth, height: .greatestFiniteMagnitude)
let calculatedSize = label.sizeThatFits(constrainedSize)
label.frame = CGRect(x: padding, y: padding, width: targetWidth, height: calculatedSize.height)
3. Calculate Height Ahead of Time with boundingRect(with:options:attributes:context:)
When calculating cell heights inside traditional UITableViewDelegate methods (like tableView(_:heightForRowAt:) without automatic dimensioning) or inside custom collection view layouts, compute the bounding box directly from the String or NSAttributedString.
import UIKit
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(
with: constraintRect,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: font],
context: nil
)
return ceil(boundingBox.height)
}
}
// Usage example:
let text = "Since the alien army vastly outnumbers the team, players must use the post-apocalyptic world to their advantage."
let font = UIFont.systemFont(ofSize: 16)
let dynamicHeight = text.height(withConstrainedWidth: 300, font: font)
Always wrap the returned height with ceil() to avoid fractional pixel truncation, which can cause UIKit to cut off the bottom of the last line of text.
4. Enable Dynamic Height in UITableView or UICollectionView Cells
If the label is inside a UITableViewCell, you do not need to calculate frames manually. Configure your Auto Layout constraints from the label to all four sides of the cell’s contentView.
import UIKit
final class DynamicTextCell: UITableViewCell {
static let reuseIdentifier = "DynamicTextCell"
let label: UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.numberOfLines = 0
lbl.lineBreakMode = .byWordWrapping
return lbl
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In your view controller, ensure the table view is set to use dynamic dimensions:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60
How to prevent it
- Never hardcode height constraints on labels displaying dynamic or localized text; allow
intrinsicContentSizeto manage vertical sizing. - Always configure
numberOfLines = 0as standard practice for dynamic content, descriptions, and user-generated text. - Avoid legacy sizing methods such as
sizeWithFont:, which were deprecated in iOS 7; useboundingRect(with:options:attributes:context:)or Auto Layout instead. - Round up manual measurements using
ceil()whenever you calculate frames programmatically to prevent subpixel clipping on high-density displays. - Verify Auto Layout compression resistance by ensuring the label’s vertical content compression resistance priority is set to
.required(1000) or.defaultHigh(750) if other views compete for vertical space.
FAQ
Why does my multiline UILabel still truncate text even with numberOfLines set to 0?
A label will truncate if an explicit height constraint is active or if its container restricts vertical expansion. Check your storyboard or code to confirm that no rigid height constraint or fixed bottom constraint is preventing the label from expanding downward.
What is the difference between sizeToFit() and sizeThatFits(_:)?
sizeToFit() executes immediately and alters both the bounds and frame of the target view to fit its content. In contrast, sizeThatFits(_:) calculates and returns the optimal CGSize without modifying the view’s current frame, allowing you to selectively apply only the height.
Why does sizeToFit() shrink the width of my label?
Calling sizeToFit() recalculates both dimensions. If the text does not fill the entire horizontal space of the label’s existing frame, UIKit shrinks the width to hug the text. To keep a fixed width, assign the original width back to label.frame.size.width immediately after calling sizeToFit(), or use sizeThatFits(CGSize(width: targetWidth, height: .greatestFiniteMagnitude)).
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
