I've been able to render NSAttributedString
s via UIViewRepresentable
which works great until I wrap the view in a ScrollView
.
When placed inside the ScrollView
, the NSAttributedString
view stops rendering.
I've tried some other methods that replace an NSAttributedString
with adding multiple Text()
views together to get formatting which works inside the ScrollView
and supports italics and monospace font
. Unfortunately this doesn't work for links inside text blocks, which means I still need an NSAttributedString
.
import SwiftUI
struct TextWithAttributedString: UIViewRepresentable {
var attributedString: NSAttributedString
init(_ attributedString: NSAttributedString) {
self.attributedString = attributedString
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView(frame: .zero)
textView.attributedText = self.attributedString
textView.isEditable = false
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.attributedText = self.attributedString
}
}
let exampleText = """
Fugiat id blanditiis et est culpa voluptas. Vivamus aliquet enim eu blandit blandit. Sit eget praesentium maxime sit molestiae et alias aut.
"""
struct NSAttributedStringView: View {
var body: some View {
// Note: when uncommented, the view breaks
// ScrollView {
TextWithAttributedString(NSAttributedString(string: exampleText))
// }
}
}
struct NSAttributedStringView_Previews: PreviewProvider {
static var previews: some View {
NSAttributedStringView()
.previewLayout(.sizeThatFits)
}
}
Edit: I tried using the wrapped UITextView
with the text
property set instead of the attributeText
property, but this also fails to render in the ScrollView
, so the issue seems to be the UITextView
, not the NSAttributedString
.
So the question is, how do we get the UITextView
to work in a ScrollView
?
We will explore a ScrollView, UIScrollView equivalent in SwiftUI. List view is a view that contains child views and displays them in a scrollable manner. SwiftUI offers two views with this capability, ScrollView and List. In the previous post, we learned two ways to populate a list view's content.
NSAttributedString is a class in UIKit representing a string with associated attributes such as bold, underscore, or color for portions of its text. Example of NSAttributedString. In SwiftUI, we don't have an equivalent view that matched the power of NSAttributedString, but we got something similar.
List view is a view that contains child views and displays them in a scrollable manner. SwiftUI offers two views with this capability, ScrollView and List. In the previous post, we learned two ways to populate a list view's content.
In 2020, SwiftUI added the ability to concatenate Text views. Adding two Text views together results in a new Text view: struct ContentView: View { var body: some View { Text("Hello ") + Text("world!") } } However, not all modifiers will work, only those that return Text.
The reason is that SwiftUI ScrollView
requires defined content size, but used UITextView
is itself a UIScrollView
and detects content based on available space in parent view. Thus it happens cycle of undefined sizes.
Here is a simplified demo of possible approach how to solve this. The idea is to calculate content size of UITextView
and pass it to SwiftUI...
struct TextWithAttributedString: UIViewRepresentable {
@Binding var height: CGFloat
var attributedString: NSAttributedString
func makeUIView(context: Context) -> UITextView {
let textView = UITextView(frame: .zero)
textView.isEditable = false
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.attributedText = self.attributedString
// calculate height based on main screen, but this might be
// improved for more generic cases
DispatchQueue.main.async { // << fixed
height = textView.sizeThatFits(UIScreen.main.bounds.size).height
}
}
}
struct NSAttributedStringView: View {
@State private var textHeight: CGFloat = .zero
var body: some View {
ScrollView {
TextWithAttributedString(height: $textHeight, attributedString: NSAttributedString(string: exampleText))
.frame(height: textHeight) // << specify height explicitly !
}
}
}
If you want to use Asperi's TextWithAttributedString
as a child view, replace
height = textView.sizeThatFits(UIScreen.main.bounds.size).height
by
DispatchQueue.main.async {
height = textView.sizeThatFits(textView.visibleSize).height
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With