I was able to get this to work by binding a variable in the TextView that is used by the encapsulating view to set the frame height. Here is the minimal TextView implementation:
struct TextView: UIViewRepresentable {
@Binding var text: String?
@Binding var attributedText: NSAttributedString?
@Binding var desiredHeight: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
uiTextView.delegate = context.coordinator
// Configure text view as desired...
uiTextView.font = UIFont(name: "HelveticaNeue", size: 15)
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if self.attributedText != nil {
uiView.attributedText = self.attributedText
} else {
uiView.text = self.attributedText
}
// Compute the desired height for the content
let fixedWidth = uiView.frame.size.width
let newSize = uiView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
DispatchQueue.main.async {
self.desiredHeight = newSize.height
}
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ view: TextView) {
self.parent = view
}
func textViewDidEndEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.text = textView.text
self.parent.attributedText = textView.attributedText
}
}
}
}
The key is the binding of desiredHeight, which is computed in updateUIView using the UIView sizeThatFits method. Note that this is wrapped in a DispatchQueue.main.async block to avoid the SwiftUI "Modifying state during view update" error.
I can now use this view in my ContentView:
struct ContentView: View {
@State private var notes: [String?] = [
"This is a short Note",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet consectetur. Morbi enim nunc faucibus a. Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero.",
]
@State private var desiredHeight: [CGFloat] = [0, 0]
var body: some View {
List {
ForEach(0..<notes.count, id: .self) { index in
TextView(
desiredHeight: self.$desiredHeight[index],
text: self.$notes[index],
attributedText: .constant(nil)
)
.frame(height: max(self.desiredHeight[index], 100))
}
}
}
}
Here I have a couple of notes in a String array, along with an array of desiredHeight values to bind to the TextView. The height of the TextView is set in the frame modifier on the TextView. In this example, I also set a minimum height to give some space for the intial edit. The frame height only updates when one of the State values (in this case the notes) changes. In the implementation of the TextView here, this only occurs when editing ends on the text view.
I tried updating the text in the textViewDidChange delegate function in the Coordinator. This updates the frame height as you add text, but makes it so that you can only ever type text at the end of the TextView since updating the view resets the insertion point to the end!
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…