Clifton Labrum
Clifton Labrum

Reputation: 14168

Resizable UITextView Has sizeThatFits That Gives Wrong Size in UIViewRepresentable

I am using a UITextView in a SwiftUI app in order to get a list of editable, multiline text fields based on this answer: How do I create a multiline TextField in SwiftUI?

I use the component in SwiftUI like this:

@State private var textHeight: CGFloat = 0
...
GrowingField(text: $subtask.text ?? "", height: $textHeight, changed:{
  print("Save...")
})
.frame(height: textHeight)

The GrowingField is defined like this:

struct GrowingField: UIViewRepresentable {
  @Binding var text: String
  @Binding var height: CGFloat
  var changed:(() -> Void)?

  func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    textView.backgroundColor = .orange //For debugging
    //Set the font size and style...
  
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    return textView
  }

  func updateUIView(_ uiView: UITextView, context: Context) {
    if uiView.text != self.text{
      uiView.text = self.text
    }
    recalculateHeight(textView: uiView, height: $height)
  }

  func recalculateHeight(textView: UITextView, height: Binding<CGFloat>) {
    
    let newSize = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))

    if height.wrappedValue != newSize.height {
      DispatchQueue.main.async {
        height.wrappedValue = newSize.height
      }
    }
  }

  //Coordinator and UITextView delegates...
}

The problem I'm having is that sizeThatFits calculates the correct height at first, then replaces it with an incorrect height. If I print the newSize inside recalculateHeight() it goes like this when my view loads:

(63.0, 34.333333333333336) <!-- Right
(3.0, 143.33333333333334) <!-- Wrong
(3.0, 143.33333333333334) <!-- Wrong

I have no idea where the wrong size is coming from, and I don't know why the right one is replaced. This is how it looks with the height being way too big:

enter image description here

If I make a change to it, the recalculateHeight() method gets called again via textViewDidChange() and it rights itself:

enter image description here

This is really hacky, but if I put a timer in makeUIView(), it fixes itself as well:

//Eww, gross...
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
  recalculateHeight(view: textView, height: $height)
}

Any idea how I can determine where the incorrect sizeThatFits value is coming from and how I can fix it?

Upvotes: 2

Views: 1040

Answers (1)

Clifton Labrum
Clifton Labrum

Reputation: 14168

It took me a long time to arrive at a solution for this. It turns out the UITextView sizing logic is good. It was a parent animation that presents my views that was causing updateUIView to fire again with in-transition UITextView size values.

By setting .animation(.none) on the parent VStack that holds all my text fields, it stopped the propagation of the animation and now it works. 🙂

Upvotes: 2

Related Questions