Ievgen Leichenko
Ievgen Leichenko

Reputation: 1317

UITextView does not adjust size when used in SwiftUI

My ultimate goal is to display html content in SwiftUI. For that I am using UIKit's UITextView (I can't use web view, because I need to control font and text color). This is the entire code of the view representable:

struct HTMLTextView: UIViewRepresentable {

private var htmlString: String
private var maxWidth: CGFloat = 0
private var font: UIFont = .systemFont(ofSize: 14)
private var textColor: UIColor = .darkText

init(htmlString: String) {
    self.htmlString = htmlString
}

func makeUIView(context: UIViewRepresentableContext<HTMLTextView>) -> UITextView {
    let textView = UITextView()
    textView.isScrollEnabled = false
    textView.isEditable = false
    textView.backgroundColor = .clear
    update(textView: textView)
    return textView
}

func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<HTMLTextView>) {
    update(textView: textView)
}

func sizeToFit(width: CGFloat) -> Self {
    var textView = self
    textView.maxWidth = width
    return textView
}

func font(_ font: UIFont) -> Self {
    var textView = self
    textView.font = font
    return textView
}

func textColor(_ textColor: UIColor) -> Self {
    var textView = self
    textView.textColor = textColor
    return textView
}

// MARK: - Private

private func update(textView: UITextView) {
    textView.attributedText = buildAttributedString(fromHTML: htmlString)
    
    // this is one of the options that don't work
    let size = textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
    textView.frame.size = size
}

private func buildAttributedString(fromHTML htmlString: String) -> NSAttributedString {
    let htmlData = Data(htmlString.utf8)
    let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]
    let attributedString = try? NSMutableAttributedString(data: htmlData, options: options, documentAttributes: nil)
    let range = NSRange(location: 0, length: attributedString?.length ?? 0)
    attributedString?.addAttributes([.font: font,
                                     .foregroundColor: textColor],
                                    range: range)
    
    return attributedString ?? NSAttributedString(string: "")
}
}

It is called from the SwiftUI code like this:

HTMLTextView(htmlString: "some string with html tags")
        .font(.systemFont(ofSize: 15))
        .textColor(descriptionTextColor)
        .sizeToFit(width: 200)

The idea is that the HTMLTextView would stick to the width (here 200, but in practice - the screen width) and grow vertically when the text is multiline.

The problem is whatever I do (see below), it is always displayed as a one line of text stretching outside of screen on the left and right. And it never grows vertically.

The stuff I tried:

Nothing helped. Any advice on how could I solve this will be very welcome!

P.S. I can't use SwiftUI's AttributedString, because I need to support iOS 14.

UPDATE:

I have removed all the code with maxWidth and calculating size. And added textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) when creating the textView in makeUIView(context:). This kind of solved the problem, except for this: even though the height of the text view is correct, the last line is not visible; if I rotate to landscape, it becomes visible; rotate to portrait - not visible again.

UPDATE 2:

After some trial and error I figured out that it is ScrollView to blame. HTMLTextView is inside VStack, which is inside ScrollView. When I remove scroll view, everything sizes correctly. The problem is, I need scrolling when the content is too long.

Upvotes: 1

Views: 1442

Answers (1)

Ievgen Leichenko
Ievgen Leichenko

Reputation: 1317

So, in the end, I had to move calculating the size that the attributed string would take in the text view with the given font/size etc into the view model, and then set .frame(width:, height:) to those values.

Not ideal, as the pre-calculated height seems a little bit larger than the actual text's height, but could not find better solution for now.

Update (for readability):

I calculate the actual size in view model (calculateDescriptionSize(limitedToWidth maxWidth:), and then I use the result on the Swift UI view:

HTMLTextView(htmlString: viewModel.attributedDescription)
    .frame(width: maxWidth, height: viewModel.calculateDescriptionSize(limitedToWidth: maxWidth).height) 

where HTMLTextView is my custom view wrapping the UIKit text view.

And this is the size calculation:

func calculateDescriptionSize(limitedToWidth maxWidth: CGFloat) -> CGSize {
    // source: https://stackoverflow.com/questions/54497598/nsattributedstring-boundingrect-returns-wrong-height
    
    let textStorage = NSTextStorage(attributedString: attributedDescription)
    let size = CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)
    let boundingRect = CGRect(origin: .zero, size: size)

    let textContainer = NSTextContainer(size: size)
    textContainer.lineFragmentPadding = 0

    let layoutManager = NSLayoutManager()
    layoutManager.addTextContainer(textContainer)
    
    textStorage.addLayoutManager(layoutManager)
    layoutManager.glyphRange(forBoundingRect: boundingRect, in: textContainer)
   
    let rect = layoutManager.usedRect(for: textContainer)
    
    return rect.integral.size
}

Upvotes: 0

Related Questions