How to check if Text is truncated in SwiftUI?
When displaying text of various lengths, our design needs to be clear on whether there should be a maximum number of displayable lines, or if the full text should always be shown.
Text
doesn't always behave predictably: sometimes the text gets truncated for no apparent reason, despite having plenty of space at our disposal (e.g. within Form
s and List
s).
In this article, let's have a look at how we can deal with these scenarios, and more.
lineLimit
Text
offers the instance method lineLimit(_:)
:
// This text instance won't exceed 3 rows.
Text(myString).lineLimit(3)
This method guarantees that our text instance will not exceed the given number of rows.
We can also pass nil
as the lineLimit
parameter, asking Text
to take as many rows as needed:
Text(myString).lineLimit(nil)
Unfortunately, passing nil
means that Text
will follow its default behavior and nothing more:
our Text
will still get truncated if SwiftUI decides that this is the right thing to do.
If the text is "five lines long" and we set
.lineLimit(3)
, this doesn't guarantee thatText
will take three rows, it just guarantees thatText
won't exceed three lines.
fixedSize(horizontal:vertical:)
fixedSize, which we used multiple times, lets any view take as much space as needed, completely disregarding the proposed size.
fixedSize
accepts two booleans, horizontal
and vertical
, letting us decide whether the view should disregard both axes proposed size, just one axis, or neither.
fixedSize
also comes with a conveniencefixedSize()
method, which is equivalent tofixedSize(horizontal: true, vertical: true)
.
To my knowledge, using Text
with .fixedSize
is the only way to guarantee Text
to be always fully displayed (if you're aware of other ways, please let me know!).
// This Text will respect the proposed horizontal space
// and take as much vertical space as needed.
Text(myLongString).fixedSize(horizontal: false, vertical: true)
There's a catch: while using .fixedSize
guarantees the full text to be displayed entirely, if there actually is not enough space, this will break the UI:
Rectangle()
.stroke()
.frame(width: 200, height: 100)
.overlay(Text(myLongString).fixedSize(horizontal: false, vertical: true))
Putting it all together
Now that we have covered the main two methods, let's see how we can answer the "How to check if Text is truncated?" question.
Similarly to UIKit, what we will need to know is whether the intrinsic size of our Text
is the same as the actual size of the Text
in the layout.
To get both the intrinsic and the actual size of our Text
we will need to add both cases in our view hierarchy, however we only want to display one of these two cases/Text
s, a trick to hide views while still computing their layout is to:
- add them as a
background
of another view, background views don't participate in the size of their parent - apply the
hidden()
modifier on them, hidden views are not drawn by SwiftUI
Here's our final layout:
Text(myString)
.lineLimit(myLineLimit)
.background(
Text(myString)
.fixedSize(horizontal: false, vertical: true)
.hidden()
)
We now need to get the sizes of both Text
s, to do so we will use readSize
, which we introduced here:
Text(myString)
.lineLimit(myLineLimit)
.readSize { size in
print("truncated size: \(size)")
}
.background(
Text(myString)
.fixedSize(horizontal: false, vertical: true)
.hidden()
.readSize { size in
print("intrinsic size: \(size)")
}
)
Lastly, we can save those two values in our view and use them at will:
struct TruncableText: View {
let text: Text
let lineLimit: Int?
@State private var intrinsicSize: CGSize = .zero
@State private var truncatedSize: CGSize = .zero
let isTruncatedUpdate: (_ isTruncated: Bool) -> Void
var body: some View {
text
.lineLimit(lineLimit)
.readSize { size in
truncatedSize = size
isTruncatedUpdate(truncatedSize != intrinsicSize)
}
.background(
text
.fixedSize(horizontal: false, vertical: true)
.hidden()
.readSize { size in
intrinsicSize = size
isTruncatedUpdate(truncatedSize != intrinsicSize)
}
)
}
}
TruncableText
invokes an isTruncatedUpdate
block with the latest truncated state at every size change, this information can then be used to adapt the UI in various situations.
For example, here's a view that displays a "Show All" button when the Text
content is truncated, and displays the full text when tapped:
struct ContentView: View {
@State var isTruncated: Bool = false
@State var forceFullText: Bool = false
var body: some View {
VStack {
if forceFullText {
text
.fixedSize(horizontal: false, vertical: true)
} else {
TruncableText(
text: text,
lineLimit: 3
) {
isTruncated = $0
}
}
if isTruncated && !forceFullText {
Button("show all") {
forceFullText = true
}
}
}
.padding()
}
var text: Text {
Text(
"Introducing a new kind of fitness experience. One that dynamically integrates your personal metrics from Apple Watch, along with music from your favorite artists, to inspire like no other workout in the world."
)
}
}
The complete gist can be found here.
Conclusions
Finding out whether a displayed text is truncated wasn't easy in UIKit and, for the moment, it's not straightforward in SwiftUI neither.
Fortunately, SwiftUI provides us all the tools needed to overcome this limitation: have you found yourself in a similar situation? how did you solve it? I'd love to know!
Thank you for reading and stay tuned for more articles!