0

I want a UITextView with its height wrapped to the content inside. And this solution works just fine. However, it breaks when there are multiple such views, say, in a VStack.

In iOS 16 UIViewRepresentable has a method sizeThatFits(_:uiView:context:) that works perfectly! It also doesn't require us to deal with the dyanmicHeight property. How do I replicate this behaviour for versions below iOS 16?

Here is a sample app to test it out. This code would run correctly for iOS 16 but not otherwise.

The problem that I found was that the height State in HTMLTextView does not update when dynamicHeight in HelperTextView is changed

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            VStack(spacing: 0) {
                Group {
                    HTMLTextView(html: dummyHtml)
                    HTMLTextView(html: dummyHtml)
                    HTMLTextView(html: dummyHtml)
                }
                .border(Color.red)
            }
        }
    }
}

private let dummyHtml = """
<html>
    <body>
        <h1>Hello, world!</h1>
        <span>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididurt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.</span>
        <a href="https://example.org">Example</a>
    </body>
</html>
"""

private struct HelperTextView: UIViewRepresentable {
    let htmlString: String
    
    @available(iOS, deprecated: 16, message: "This is not required as sizeThatFits(_:uiView:context:) was introduced. It can be safely deleted")
    let parentSize: CGSize
    
    @available(iOS, deprecated: 16, message: "This is not required as sizeThatFits(_:uiView:context:) was introduced. It can be safely deleted")
    @Binding var dynamicHeight: CGFloat
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        configureStyling(textView)
        
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textView.attributedText = try? configureHtmlText()
        
        if #unavailable(iOS 16) {
            DispatchQueue.main.async {
                dynamicHeight = textView.sizeThatFits(CGSize(
                    width: parentSize.width,
                    height: .greatestFiniteMagnitude
                )).height
            }
        }
        return textView
    }
    
    func updateUIView(_ textView: UITextView, context: Context) {
        
    }
    
    @available(iOS 16, *)
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
        uiView.sizeThatFits(CGSize(
            width: proposal.width ?? .greatestFiniteMagnitude,
            height: proposal.height ?? .greatestFiniteMagnitude
        ))
    }
    
    private func configureStyling(_ textView: UITextView) {
        textView.isEditable = false
        textView.isSelectable = false
        textView.isScrollEnabled = false
        textView.backgroundColor = .clear
        textView.textContainerInset = .zero
    }
    
    private func configureHtmlText() throws -> NSAttributedString {
        let htmlAttrString = try NSMutableAttributedString(
            data: Data(htmlString.utf8),
            options: [.documentType: NSAttributedString.DocumentType.html],
            documentAttributes: nil
        )
        let range = NSRange(0..<htmlAttrString.length)
        htmlAttrString.addAttribute(.foregroundColor, value: UIColor.label, range: range)
        return htmlAttrString
    }
}

struct HTMLTextView: View {
    let html: String
    
    @available(iOS, deprecated: 16, message: "This is not required as sizeThatFits(_:uiView:context:) was introduced. It can be safely deleted")
    @State private var height: CGFloat = .zero
    
    var body: some View {
        if #available(iOS 16, *) {
            HelperTextView(
                htmlString: html,
                parentSize: .zero,
                dynamicHeight: .constant(0)
            )
        } else {
            GeometryReader { geo in
                HelperTextView(
                    htmlString: "View Height: \(height)" + html,
                    parentSize: geo.size,
                    dynamicHeight: $height
                )
            }
            .frame(height: height)
        }
    }
}
Expected Actual
ikroop
  • 96
  • 1
  • 6

0 Answers0