swiftuiswiftui-text

Add quotes for truncated multiline text


I'm using Text view to display some text. I need to limit the line number to 3, and text should be truncated at the end (trail mode). As you could expect, if you add the 2 quotes (opening and closing), then truncate the text, the last one could be truncated too and won't be displayed.

I tried to concatenate 3 Text (code below) but it will not compile as the lineLimit() modifier returns a some View not a Text.

And finally, using a HStack is not a good idea as then it will add a blank between the text and the closing quote.

Text(Image(systemName: "quote.opening"))
  .font(.caption)
  .baselineOffset(4)
+
Text("Some long string"))
  .font(.subheadline)
  .lineLimit(3)
+
Text(Image(systemName: "quote.closing"))
  .font(.caption)
  .baselineOffset(4)

Solution

  • It might be possible to implement a precise solution for this problem using TextRenderer, but it probably won't be easy.

    However, an approximate solution can be based on the following approach:

    So in the case of non-truncated text, the quotes are shown in the right places. In the case of truncated text, the end of the text is covered by the overlay. This is not perfect, but it might be fit for purpose.

    Here is how a general purpose view can be implemented this way:

    struct QuotedText: View {
        let text: String
        var lineLimit: Int?
        @State private var isTruncated = false
    
        private var fullText: some View {
            (
                Text(Image(systemName: "quote.opening")) +
                Text(text) +
                Text(Image(systemName: "quote.closing"))
            )
            .imageScale(.small)
        }
    
        var body: some View {
            fullText
                .lineLimit(lineLimit)
                .background {
                    GeometryReader { proxy in
                        fullText
                            .fixedSize(horizontal: false, vertical: true)
                            .hidden()
                            .onGeometryChange(for: CGFloat.self, of: \.size.height) { height in
                                isTruncated = height > proxy.size.height
                            }
                    }
                }
                .overlay(alignment: .trailingLastTextBaseline) {
                    if isTruncated {
                        (
                            Text("...") +
                            Text(Image(systemName: "quote.closing"))
                        )
                        .imageScale(.small)
                        .padding(.leading, 10)
                        .background {
                            LinearGradient(
                                colors: [.clear, Color(.systemBackground)],
                                startPoint: .leading,
                                endPoint: UnitPoint(x: 0.3, y: 0.5)
                            )
                        }
                    }
                }
        }
    }
    

    Example use:

    VStack(spacing: 20) {
        QuotedText(text: "Some short text")
        QuotedText(text: loremIpsum, lineLimit: 3)
            .font(.subheadline)
        QuotedText(text: "The quick brown fox jumps over the lazy dog", lineLimit: 2)
            .font(.title)
    }
    .multilineTextAlignment(.leading)
    .padding()
    

    Screenshot