How to get dynamic Text height for a ScrollView with SwiftUI

Viewed 8984

this gives me dynamic text height correctly

import SwiftUI
struct ContentView : View {
    var body: some View {
        List {
            Text("This is some very long text can we can see scrolls past two lines ").lineLimit(nil)   
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

enter image description here

But the following truncates the Text. How do i get dynamic height with the following?

import SwiftUI
struct ContentView : View {
    var body: some View {
        GeometryReader { reader in
            ScrollView {
                Text("This is some very long text can we can see scrolls past two lines ")
                    .lineLimit(nil)
                    .frame(width: reader.size.width)
            }
        }
    }
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

enter image description here

5 Answers

You need to prevent truncation using fixedSize modifier. In your case it is like:

Text("This is some very long text can we can see scrolls past two lines ")
 .lineLimit(nil)
 .fixedSize(horizontal: false, vertical: true) //** It keeps 'width size' and expands 'height size'
 .frame(width: reader.size.width)
import SwiftUI

struct ContentView : View {

    let veryLongText = "Very long text..."
    let fontName = "System Font"
    let fontSize: Length = 12

    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                Text(veryLongText)
                    .lineLimit(nil)
                    .font(.custom(fontName, size: fontSize))
                    .frame(width: geometry.size.width, height: veryLongText.textHeightFrom(width: geometry.size.width, fontName: fontName, fontSize: fontSize))
            }
        }
        .padding(.horizontal, 0.5 * fontSize)
    }
}

extension String {

    func textHeightFrom(width: CGFloat, fontName: String = "System Font", fontSize: CGFloat = .systemFontSize) -> CGFloat {

        #if os(macOS) 

        typealias UXFont = NSFont
        let text: NSTextField = .init(string: self)
        text.font = NSFont.init(name: fontName, size: fontSize)

        #else

        typealias UXFont = UIFont
        let text: UILabel = .init()
        text.text = self
        text.numberOfLines = 0

        #endif

        text.font = UXFont.init(name: fontName, size: fontSize)
        text.lineBreakMode = .byWordWrapping
        return text.sizeThatFits(CGSize.init(width: width, height: .infinity)).height
    }
}

Might be a ScrollView bug. I'm honestly not sure why your code doesn't work, but you can get the desired result using:

struct ContentView : View {
    var body: some View {
        GeometryReader { reader in
            ScrollView(alwaysBounceVertical: true) {
                ZStack(alignment: .top) {
                    Color.clear.edgesIgnoringSafeArea(.all)
                    Text("This is some very long text can we can see scrolls past two lines ")
                        .lineLimit(nil)
                }.frame(width: reader.size.width, height: reader.size.height)
            }
        }
    }
}

I added alwaysBounceVertical to the scroll view so you can see that the view scrolls. It's not necessary though.

In my opinion this is a bug, as I cannot find the rationale of its behavior.

Let me explain:

If you do not have a ScrollView, we know that forcing the width of a Text view to a specific width and using the lineLimit(nil), will make the text view expand vertically to accommodate the text. If instead, you have lineLimit(1), the ellipsis will show to truncate the contents of the view.

Now, if we have the Text() inside a ScrollView, and we force the width to a specific size and set lineLimit(nil), there are two acceptable outcomes:

  1. The text view respects the new width and expands vertically.
  2. The text view does not respect the width but let you scroll horizontally. Not at all a good choice, but it could be a decision made by who developed the framework.

However, what actually happens, is that the text view respects the new width size (as made evident by the ellipsis), but it won't expand vertically in spite of the lineLimit(nil). There is no reason for that, since that behavior can already be achieved by using lineLimit(1). This is why I think this is definitely a bug.

You should file a bug report with Apple.

This appears to have been addressed with iOS 15.0. But if you want to support users on iOS 14, then you could add a hack view modifier

@available(iOS 14.0, *)
fileprivate struct LineLimitHackForiOS14: ViewModifier {

    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            return AnyView(content)
        }
        return AnyView(content.fixedSize(horizontal: false, vertical: true))
    }
}

@available(iOS 14.0, *)
extension View {
    fileprivate func applyHackToPreventTextTruncationInScrollViews() -> some View {
        self.lineLimit(nil).modifier(LineLimitHackForiOS14())
    }
}

Where code could be used as

Text(self.text).applyHackToPreventTextTruncationInScrollViews()
Related