Kampf gegen Text-Kürzung in SwiftUI
Die Entwicklung der mobilen Vector Bible-App war mein erstes Projekt mit Swift und SwiftUI. Als Einsteiger stolpert man dabei natürlich über ein paar Probleme.
Eines der unangenehmeren war definitiv das automatische Kürzen von Text. Deshalb möchte ich teilen, warum ich dieses Problem meiner Meinung nach hatte, welche Vorschläge ich gefunden habe und was letztlich funktioniert hat.
Das Problem
Eine der großartigen Eigenschaften von SwiftUI ist, dass es Entwicklern vieles automatisch abnimmt. Theoretisch können wir dadurch Apps leichter und schneller bauen, die nativ sind und sich auch so anfühlen.
Eines dieser automatischen Verhaltensweisen ist die Text-Kürzung.
Die Idee dahinter ist einfach: Man nimmt eine Text-Komponente, platziert sie irgendwo und gibt ihr vielleicht einige Größenparameter.
SwiftUI kümmert sich dann um die Darstellung – der Text wird automatisch umbrochen und mit einer Ellipse (...) gekürzt, wenn er innerhalb der gegebenen Größenbeschränkungen nicht vollständig angezeigt werden kann.
In der Praxis gibt es allerdings keinen unkomplizierten Weg, diese automatische Kürzung vollständig zu deaktivieren.
In Vector Bible hatte ich mehrere Textelemente vertikal in einer Liste gestapelt. Die Liste selbst war von weiteren Layout- und UI-Komponenten umgeben. Zusammen waren diese Textelemente meist höher als der Bildschirm, weshalb das gesamte Konstrukt irgendwo scrollbar gemacht wurde. Nennen wir es von nun an unseren Text-Stack. Ärgerlicherweise scheinen in diesem Text-Stack irgendwo Höhenbeschränkungen hineingeraten zu sein. Das Problem: Ich wusste weder warum noch wo, und auch nicht, wie ich das herausfinden könnte.

Vorgeschlagene Lösungen
Das war für mich eine ziemlich harte Nuss. Ich habe Stunden damit verbracht, zu recherchieren und vorgeschlagene Lösungen auszuprobieren.
Der beliebteste Vorschlag war, .fixedSize(horizontal: false, vertical: true) zu verwenden.
Damit würden vertikale Beschränkungen ignoriert und der Text sollte sich über mehrere Zeilen umbrechen können.
Dadurch verschwand die Kürzung aber nur an manchen Stellen und tauchte dafür an anderen wieder auf.
Manche empfahlen, .fixedSize() vor bestimmten anderen Modifiers anzuwenden, andere rieten zu .layoutPriority(1).
Ein eher schlechter Vorschlag, den ich leider oft gesehen habe, war .minimumScaleFactor(0.5), damit der Text sich innerhalb der gegebenen Beschränkungen automatisch verkleinert.
Das kann in manchen Situationen passen, aber offensichtlich nicht, wenn viele gleich wichtige Textelemente vertikal gestapelt sind.
Ich habe auch versucht, ViewThatFits irgendwie einzusetzen, aber auch das hat mein Problem nicht gelöst.
Wie ich es gelöst habe
Nachdem ich also unzählige Stunden frustriert verbracht hatte und der Lösung keinen Schritt näher gekommen war, entschied ich mich, in den sauren Apfel zu beißen und mit UIKit eine eigene, in UIViewRepresentable verpackte Text-Komponente zu erstellen.
Ich hatte zuvor noch nie UIKit-Code geschrieben, deshalb ist das hier im Grunde vollständig KI-generiert.
Natürlich hat die Komponente ein paar Eigenheiten.
Zum Beispiel muss man die maximale Breite fast immer manuell übergeben.
Außerdem sieht sie in manchen Situationen einfach seltsam aus, weshalb ich diese Grenzen beim Design berücksichtigen musste.
Jedenfalls hat die Verwendung dieser Komponente anstelle von Text mein Problem mit Text-Kürzungen ein für alle Mal gelöst.
Der Code
Nutzt ihn gerne, wie es für euch passt. Ich hoffe, er hilft anderen, die mit diesem Problem kämpfen.
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
}
}
}