I am trying to figure out how to make UITextView size dependent on it's contents in SwiftUI. I wrapped the UITextView in UIViewRepresentable
as following:
struct TextView: UIViewRepresentable {
@Binding var showActionSheet: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
uiTextView.delegate = context.coordinator
uiTextView.font = UIFont(name: "HelveticaNeue", size: 15)
uiTextView.isScrollEnabled = true
uiTextView.isEditable = true
uiTextView.isUserInteractionEnabled = true
uiTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
uiTextView.isEditable = false
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = prepareText(question: question)
var frame = uiView.frame
frame.size.height = uiView.contentSize.height
uiView.frame = frame
}
func prepareText(text: string) -> NSMutableAttributedString {
...................
return attributedText
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ view: TextView) {
self.parent = view
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
parent.showActionSheet = true
return false
}
}
}
Further, I tried to change the frame size in updateUIView based on it's content size, but it did not have any effect. I suppose that at this stage, the view is not even layout and its frame is being overridden somewhere else. I would really appreciate if someone could point me in a correct direction.
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!
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