How to Get Text to Wrap in a Uilabel (Via Uiviewrepresentable) Without Having a Fixed Width

How can I get text to wrap in a UILabel (via UIViewRepresentable) without having a fixed width?

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)
}
}
}

Size a UILabel in SwiftUI via UIViewRepresentable like Text to wrap multiple lines

The problem here is in ScrollView which requires definite height, but representable does not provide it. The possible solution is to dynamically calculate wrapped text height and specify it explicitly.

Note: as height is calculated dynamically it is available only in run-time, so cannot be tested with Preview.

Tested with Xcode 12 / iOS 14

demo

struct LabelView: View {
var text: String

@State private var height: CGFloat = .zero

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

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

func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
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
}
}
}
}

backup

Multiline label with UIViewRepresentable

There's a few things going on here.

  • InformationView doesn't have an intrinsicContentSize. SwiftUI relies on this property to determine a UIView's ideal size.
  • No fixedSize modifier. This modifier forces a view to maintain its ideal size rather than grow to fill its parent.

You can't add an intrinsicContentSize because the content size is dynamic; it's based on the number of lines in your label.

You can't add the fixedSize modifier, because without an intrinsicContentSize, this will set the size to (0,0).

/p>

One solution is to wrap InformationView in a UIView that measures the InformationView size, and updates its own intrinsicContentSize according to that measurement.

Your InformationView should fill the width of the screen; it doesn't have an intrinsic width. On the other hand, the intrinsic height should be equal to the height of the compressed system layout size. This is "the optimal size of the view based on its constraints".

Here is the wrapper:

class IntrinsicHeightView<ContentView: UIView>: UIView {
var contentView: ContentView

init(contentView: ContentView) {
self.contentView = contentView
super.init(frame: .zero)
backgroundColor = .clear
addSubview(contentView)
}

@available(*, unavailable) required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private var contentHeight: CGFloat = .zero {
didSet { invalidateIntrinsicContentSize() }
}

override var intrinsicContentSize: CGSize {
.init(width: UIView.noIntrinsicMetric, height: contentHeight)
}

override var frame: CGRect {
didSet {
guard frame != oldValue else { return }

contentView.frame = self.bounds
contentView.layoutIfNeeded()

let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height)

contentHeight = contentView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel).height
}
}
}

This IntrinsicHeightView is a generic UIView that, when its frame changes, recalculates the height of its intrinsicContentSize according to the compressed system layout height of its content view.

Here is the update to InformationRepresenter:

struct InformationRepresenter: UIViewRepresentable {
func makeUIView(context: Context) -> IntrinsicHeightView<InformationView> {
return IntrinsicHeightView(contentView: InformationView())
}

func updateUIView(_ uiView: IntrinsicHeightView<InformationView>, context: Context) {
}
}

And finally, when using it in SwiftUI:

InformationRepresenter()
.padding()
.fixedSize(horizontal: false, vertical: true)

Using the fixedSize modifier this way allows the UIView to fill the parent, but use intrinsicContentSize.height as its height. Here is the final result. Good luck!

Using UIViewRepresentable to wrap MarqueeLabel view

It is by-default expanded by intrinsic content to full width, so MarqueeLabel just does not have what to scroll - everything is in frame.

In such cases we need to give ability to parent to shrink internal view to externally available space (by width in this case to screen)

so here is a fix - decrease resistance priority:

func makeUIView(context: UIViewRepresentableContext<UILabelView>) -> MarqueeLabel {

let label = MarqueeLabel()
label.text = text
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
// ^^^^^ << this one !!
return label

}

demo

Tested with Xcode 13.4 / iOS 15.5

How does UIViewRepresentable size itself in relation to the UIKit control inside it?

Most simple and quick fix:

demo

        SwiftUIText(text: $helperText).fixedSize()

Update:

demo2

        SwiftUIText(text: $helperText)
.fixedSize(horizontal: false, vertical: true)

SwiftUI Text scaledToFit and wrap text

try this, and similarly for the pink:

    HStack {
Text(lorem)
.frame(width: 100, height: 100) // <--- here
.allowsTightening(true)
.lineLimit(2)
.scaledToFit()
.minimumScaleFactor(0.7)
}
.frame(width: 100, height: 100)
.background(Color.green)


Related Topics



Leave a reply



Submit