How to check if Text is truncated in SwiftUI?

12 January 2021

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 Forms and Lists).

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 that Text will take three rows, it just guarantees that Text 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 convenience fixedSize() method, which is equivalent to fixedSize(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/Texts, 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 Texts, 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!

⭑⭑⭑⭑⭑

Related articles

More SwiftUI articles

Browse all