Fighting SwiftUI Text Truncation
Developing the Vector Bible mobile app was my first time using Swift and SwiftUI. Being a first-time user, naturally, means stumbling across a few problems.
One of the nastier ones definitely was text truncation. So I thought I’d share why (I believe) I had this problem, what suggestions I’ve found and what actually worked.
The Problem
One of the great things about SwiftUI is that it does a lot for the developer automatically. In theory, this means that we developers can build apps that look and feel native more easily and quickly.
One of these automatic behaviors is text truncation.
The idea behind it is simple: you take a Text component, place it somewhere, and maybe assign it some size parameters.
SwiftUI then handles displaying the content for you — it wraps over automatically and truncates the text with an ellipsis (...
) if it can’t be fully displayed within the given size constraints.
However, in practice there’s no straightforward way to completely disable this automatic text truncation.
In Vector Bible, I had several text elements stacked vertically in a list. The list itself was wrapped in some other layout/UI components. All these combined text elements were usually taller than the screen, so somewhere in this construct the whole thing was made scrollable. Let’s call this our text-stack from now on. Annoyingly, in this text-stack some height limitations seem to have sneaked in somewhere. The problem: I didn’t really know why or where, and I also did not know of any way to find out.
Suggested Solutions
This was quite a tough nut to crack for me. I spent hours researching and trying suggested fixes.
The most popular solution suggested was using .fixedSize(horizontal: false, vertical: true)
.
This way, vertical constraints would be ignored, and text should be able to wrap onto multiple lines.
However, this made the truncation disappear in some spots but, in return, appear in others.
Some recommended applying .fixedSize()
before applying certain other modifiers, while others recommended using .layoutPriority(1)
.
One subpar suggestion (that I sadly saw quite often) was to use .minimumScaleFactor(0.5)
, so that the text would automatically scale within the given constraints.
This may be a good fit for some situations, but obviously not when you have many text elements of equal importance stacked vertically.
I also tried using ViewThatFits
in some way, but that also did not solve my problem.
How I solved it
So, after spending countless hours getting frustrated and getting nowhere near to solving the problem, I decided to bite the bullet and create my own Text component using UIKit wrapped inside a UIViewRepresentable
.
I have never written UIKit code before, so this is basically all AI-generated.
It has a few quirks, of course.
For example, you pretty much always have to pass the maxWidth manually.
Also, in some situations it just looks weird, so I had to design around these limitations.
Anyway, using this instead of the default Text
component solved my text truncation problem once and for all.
The Code
Feel free to use this as you see fit. I hope it can help others struggling with this problem.
import SwiftUI
struct WrappingUILabel: UIViewRepresentable {
var text: String
var font: UIFont = UIFont.preferredFont(forTextStyle: .body)
var multiplier: CGFloat = 1.0
var textColor: UIColor = .label
var textAlignment: NSTextAlignment = .natural
var applyVerseNumberStyling: Bool = true
var maxWidth: CGFloat? = nil
var lineSpacing: CGFloat? = nil
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.isUserInteractionEnabled = true
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
let scaledFont = UIFontMetrics(forTextStyle: .body)
.scaledFont(for: font.withSize(font.pointSize * multiplier))
label.font = scaledFont
label.adjustsFontForContentSizeCategory = true
label.textColor = textColor
label.textAlignment = textAlignment
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
let paragraphStyle = NSMutableParagraphStyle()
// Ensure multi-line alignment and spacing are consistent
paragraphStyle.alignment = textAlignment
if let ls = lineSpacing {
paragraphStyle.lineSpacing = ls
} else {
// Default: more spacing for verse text, tighter for plain/headings
paragraphStyle.lineSpacing = applyVerseNumberStyling ? 12 : 0
}
let attrString: NSMutableAttributedString
if applyVerseNumberStyling {
if let spaceIndex = text.firstIndex(of: " ") {
let verseNumber = String(text[..<spaceIndex])
let remainder = String(text[text.index(after: spaceIndex)...])
let verseNumberAttributed = NSMutableAttributedString(string: verseNumber + " ")
verseNumberAttributed.addAttribute(
.foregroundColor,
value: UIColor.secondaryLabel,
range: NSRange(location: 0, length: verseNumberAttributed.length)
)
let remainderAttributed = NSMutableAttributedString(string: remainder)
attrString = NSMutableAttributedString()
attrString.append(verseNumberAttributed)
attrString.append(remainderAttributed)
} else {
attrString = NSMutableAttributedString(string: text)
}
} else {
attrString = NSMutableAttributedString(string: text)
}
// Apply paragraph style
attrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attrString.length))
uiView.attributedText = attrString
let scaledFont = UIFontMetrics(forTextStyle: .body)
.scaledFont(for: font.withSize(font.pointSize * multiplier))
uiView.font = scaledFont
uiView.adjustsFontForContentSizeCategory = true
// Width for wrapping
if let maxWidth = maxWidth {
uiView.preferredMaxLayoutWidth = maxWidth
} else {
uiView.preferredMaxLayoutWidth = UIScreen.main.bounds.width - 40
}
}
}