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.

An example of text truncation

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
        }
    }
}
© 2025 Franz Josef Drexler