If I want a non-scrolling UITextView
that takes the entire width and grows and shrinks vertically to fit its text, I can do it like this in UIKit:
import UIKit
class TestViewController: UIViewController {
private lazy var textView = UITextView()
override func viewDidLoad() {
super.viewDidLoad()
guard let view = view else { return }
view.backgroundColor = .white
textView.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
textView.backgroundColor = .systemYellow
textView.isEditable = true
textView.isSelectable = true
textView.isScrollEnabled = false // We want the view to resize to fit text instead of scrolling
view.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
And it looks like this:
I want to bridge this to SwiftUI by wrapping a UITextView
with UIViewRepresentable
. I did it like this, with the text view configured exactly as it is in the UIKit example:
import SwiftUI
import UIKit
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.backgroundColor = .clear
textView.isEditable = true
textView.isSelectable = true
textView.isScrollEnabled = false // We want the view to resize to fit text instead of scrolling
// Makes the text wrap rather than extend on one line outside the parent frame
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: _text)
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
extension TextView {
class Coordinator: NSObject, UITextViewDelegate {
@Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text = textView.text
}
}
}
And used it in SwiftUI like this:
import SwiftUI
struct ContentView: View {
@State var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
var body: some View {
VStack(alignment: .leading) {
TextView(text: $text)
.background(Color.yellow)
Spacer()
}
}
}
Setting textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
successfully makes the text view wrap it's text onto multiple lines, but the height fills the entire screen:
Adding textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
does shrink the height, but now line wrapping no longer works; all the text is in one line that extends outside the frame:
I haven't found too much in the documentation or online about how UIViewRepresentable
bridges layouts from UIKit to SwiftUI. Is there any way to achieve this automatic growing and shrink to fit behavior? Or will I have to do some hacky stuff with sizeThatFits
and set the frame manually whenever the text changes?