21

Setting lineBreakMode to byWordWrapping and set numberOfLines to 0 does not seem to be sufficient:

struct MyTextView: UIViewRepresentable { func makeUIView(context: Context) -> UILabel { let label = UILabel() label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 label.text = "Here's a lot of text for you to display. It won't fit on the screen." return label } func updateUIView(_ view: UILabel, context: Context) { } } struct MyTextView_Previews: PreviewProvider { static var previews: some View { MyTextView() .previewLayout(.fixed(width: 300, height: 200)) } } 

The text does not wrap, regardless of which setting I use for lineBreakMode. The canvas preview and live preview both look like this:

Screenshot of the text not wrapping

The closest I've gotten is setting preferredMaxLayoutWidth, which does cause the text to wrap, but there doesn't seem to be a value that means "whatever size the View is".

2
  • 1
    Could you include CardTextView code in your question please? Commented Oct 20, 2019 at 10:00
  • Whoops, I'd meant to rename that MyTextView like everywhere else. The example is self-contained. The only part of the file I didn't include was import SwiftUI. Commented Oct 20, 2019 at 14:41

3 Answers 3

29

Possible solution is to declare the width as a variable on MyTextView:

struct MyTextView: UIViewRepresentable { var width: CGFloat func makeUIView(context: Context) -> UILabel { let label = UILabel() label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 label.preferredMaxLayoutWidth = width label.text = "Here's a lot of text for you to display. It won't fit on the screen." return label } func updateUIView(_ view: UILabel, context: Context) { } } 

and then use GeometryReader to findout how much space there is avaible and pass it into the intializer:

struct ExampleView: View { var body: some View { GeometryReader { geometry in MyTextView(width: geometry.size.width) } } } 
Sign up to request clarification or add additional context in comments.

3 Comments

Hello, just to have in mind, when you use a ScrollView, looks like this solution doesn't work, just put the ScrollView inside the GeometryReader like this: var body: some View { GeometryReader { geometry in ScrollView { MarkDownText(width: geometry.size.width, string: self.documentationStr) .padding([.leading, .bottom, .trailing], 10.0) .multilineTextAlignment(.leading) } } }
What about .attributedText. Does it work with it too?
@MauricioZárate how can i do this with a system image beside it for both single and multi line
24

Try to use this magic line in makeUIView() func

label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 

5 Comments

yes but it's truncating line ! I've tried it, but I have only one line truncates at the end.
This solves a problem with my UILabel UIViewRepresentable when I use it to display only 1 line with truncateTail, if I don't use this line the label covers the width of the screen, now it uses the preferredMaxLayoutWidth as expected
This solution works for me! No need to provide width from GeometryReader as suggested by the accepted answer. You just need to set your label's numberOfLines to 0 and and this line.
This and numberOfLines = 0 does the trick. No need for geometry reader.
This unfortunately doesn't work in a ScrollView. The accepted answer however does work in a ScrollView
5

I found a somehow "nasty" approach that allows a UILabel to properly wrap when used as a UIViewRepresentable (even when inside a ScrollView), without the need for GeometryReader:

Whenever creating your UILabel:

label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) label.setContentHuggingPriority(.defaultHigh, for: .vertical) 

This ensures that:

  • the label will break line and not have an infinite width
  • the label will not add grow unnecessarily in height, which may happen in some circumstances.

Then...

  • Add a width property to your UIViewRepresentable that will be used to set the preferredMaxLayoutWidth
  • Use your UIViewRepresentable into a vanilla SwiftUI.View
  • Add a GeometryReader as an overlay to prevent expansion
  • Trigger the measurement after a soft delay, modifying some state to trigger a new pass.

i.e.:

 public var body: some View { MyRepresentable(width: $width, separator: separator, content: fragments) .overlay(geometryOverlay) .onAppear { shouldReadGeometry = true } } // MARK: - Private Props @State private var width: CGFloat? @State private var shouldReadGeometry = false @ViewBuilder var geometryOverlay: some View { if shouldReadGeometry { GeometryReader { g in SwiftUI.Color.clear.onAppear { self.width = g.size.width } } } } 

OLD ANSWER:

...

In your updateUIView(_:context:):

if let sv = uiView.superview, sv.bounds.width != 0 { let shouldReloadState = uiView.preferredMaxLayoutWidth != sv.bounds.width uiView.preferredMaxLayoutWidth = sv.bounds.width if shouldReloadState { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { self.stateToggle.toggle() // a Bool @State you can add in your struct } } } 

Disclaimer: I'm not a huge fan of main.async calls, particularly when they come in combination with some arbitrary delay, but this seems to get the work done in a consistent way.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.