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 |
---|---|
![]() |
![]() |