I have a custom UITextView wrapped in SwiftUI using UIViewRepresentable. The goal is to have a dynamic height text view that grows as the user types. Here is a simplified version of my code:
import SwiftUI
struct ContentView: View {
@State private var text = populateText()
@State private var minHeight: CGFloat = 36
private let lineLimit: Int = 0
var body: some View {
NavigationStack {
ScrollView {
DynamicTextView(
text: $text,
font: UIFont.systemFont(ofSize: 16),
linesLimit: lineLimit
)
.frame(minHeight: minHeight)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray, lineWidth: 1)
}
.padding()
}
}
}
}
struct DynamicTextView: UIViewRepresentable {
@Binding var text: String
let font: UIFont
let linesLimit: Int
private var isScrollEnabled: Bool {
linesLimit > .zero ? true : false
}
func makeUIView(context: Context) -> UITextView {
let textView = CustomTextView()
textView.delegate = context.coordinator
textView.font = font
textView.text = text
textView.backgroundColor = .clear
textView.textContainer.lineFragmentPadding = 0
textView.textContainerInset = .init(top: 8, left: 8, bottom: 8, right: 8)
textView.isScrollEnabled = isScrollEnabled
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if uiView.text != text {
uiView.text = text
}
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
let width = proposal.width ?? uiView.bounds.width
let fitting = CGSize(width: width, height: .greatestFiniteMagnitude)
let calculatedHeight = uiView.sizeThatFits(fitting).height
var height = calculatedHeight
if linesLimit > 0 {
let lineHeight = font.lineHeight
let maxHeight = lineHeight * CGFloat(linesLimit) + uiView.textContainerInset.top + uiView.textContainerInset.bottom
height = min(maxHeight, calculatedHeight)
}
return CGSize(width: width, height: height)
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, UITextViewDelegate {
@Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
}
}
final class CustomTextView: UITextView {
init() {
super.init(frame: .zero, textContainer: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#Preview {
ContentView()
}
func populateText() -> String {
var text = ""
for _ in 0..<300 {
text += " Large text"
}
return text
}
The issue:
When typing a long text, if the system automatically wraps text to a new line, the entire text content jumps back to the top for a moment. The caret remains in the correct place on the new line. If I press Enter to insert a manual line break, the issue does not happen. When I continue typing, the text returns to the correct position, but the jumping is very noticeable.
Question:
Why does UITextView inside SwiftUI jump to the top when the text wraps to a new line, and how can I fix this so the text stays stable while typing?
Coordinator(self)won’t work cause the representable is a value with no lifetime not an object, where did you get that from?