2

I have a text field like this

Text("Hello, one two three four five six seven eight!")
                .frame(width:270)
                .border(.blue)

When it renders it decides to put seven and eight on the second line even though there is space for seven on the first line. Worse it decides to indent the truncated top line so it is centred within the frame.

enter image description here

enter image description here

How do I fix this so it wraps the text properly without taking into account the orphan?

Edit: Forgot to mention that I wanted this on macOS. I have tried to port it to the Mac. It does correctly left align the text but it doesn't wrap to the second line. The height of the box does get calculated accordingly though.

enter image description here

Here is my updated code:

struct NonOrphanedText: View
{
    var text: String
    
    @State private var height: CGFloat = .zero
    
    var body: some View
    {
        InternalLabelView(text: text, dynamicHeight: $height)
            .frame(maxHeight: height)
    }
    
    struct InternalLabelView: NSViewRepresentable
    {
        var text: String
        @Binding var dynamicHeight: CGFloat
        
        func makeNSView(context: Context) -> NSTextField
        {
            let label = NSTextField()
            label.isEditable = false
            label.isBezeled = false
            label.drawsBackground = false
            label.isSelectable = false
            label.maximumNumberOfLines = 5
            label.usesSingleLineMode = false
            label.lineBreakStrategy = .init()
            label.lineBreakMode = .byWordWrapping
            label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
            return label
        }
        
        func updateNSView(_ nsView: NSTextField, context: Context)
        {
            nsView.stringValue = text
            
            DispatchQueue.main.async
            {
                dynamicHeight = nsView.sizeThatFits(CGSize(width: nsView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
            }
        }
    }
}
iphaaw
  • 6,764
  • 11
  • 58
  • 83

1 Answers1

2

We need lineBreakStrategy but it is unavailable for now in SwiftUI, so possible solution is to use UILabel.

Here is a possible solution. Tested with Xcode 13.2 / iOS 15.2

demo

struct LabelView: View {
    var text: String

    @State private var height: CGFloat = .zero

    var body: some View {
        InternalLabelView(text: text, dynamicHeight: $height)
            .frame(maxHeight: height)
    }

    struct InternalLabelView: UIViewRepresentable {
        var text: String
        @Binding var dynamicHeight: CGFloat

        func makeUIView(context: Context) -> UILabel {
            let label = UILabel()
            label.numberOfLines = 0
            label.lineBreakStrategy = .init()          // << here !!
            label.lineBreakMode = .byWordWrapping
            label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

            return label
        }

        func updateUIView(_ uiView: UILabel, context: Context) {
            uiView.text = text

            DispatchQueue.main.async {
                dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This works nicely, but I forgot to mention that I wanted this on macOS. I have tried to port it to the Mac. It does correctly left align the text but it doesn't wrap to the second line. The height of the box does get calculated accordingly though. Thanks – iphaaw Apr 18 '22 at 06:54