I'm building a SwiftUI app and I want to put an editable UITextView (wrapped in UIViewRepresentable) into a SwiftUI ScrollView. The reason for this is that I have other SwiftUI content that I want to put above the text and I want this to scroll together with the text. I need a TextView because I want rich text & a toolbar.
I thought I could disable the scroll on the UITextView and give it infinite height, but this also gives it infinite width, so the text scrolls of the edge of the screen horizontally until you add a newline. I have tried setting the content size and frame size of the textview to the screen width as suggested in posts like How to disable vertical scrolling in UITextView? but I can't make it work. I can update the textview content size successfully but the changes seem to be overwritten later (see print statements in textViewDidChange).
Is there any way of achieving this? i.e. an editable UITextView inside a SwiftUI ScrollView. Thanks in advance!
import SwiftUI
struct TextView: UIViewRepresentable {
var attributedString: NSAttributedString
var fixedWidth: CGFloat
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
print("pre content size update: \(textView.contentSize)") // width is over limit
let newSize = textView.sizeThatFits(CGSize(width: parent.fixedWidth, height: CGFloat.greatestFiniteMagnitude))
textView.contentSize = CGSize(width: max(newSize.width, parent.fixedWidth), height: newSize.height)
print("post content size update: \(textView.contentSize)") // width is updated
}
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView(frame: .zero)
textView.isEditable = true
textView.isScrollEnabled = false
textView.backgroundColor = .cyan // just to make it easier to see
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.attributedText = self.attributedString
}
}
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/){
//Other content
Image(systemName: "photo")
.font(.largeTitle)
Button("A button"){
print("i'm a button")
}
//Editable textview
TextView(attributedString: NSAttributedString(string: "Here is a long string as you can see it is not wrapping but instead scrolling off the edge of the screen"), fixedWidth: geo.size.width)
.frame(width: geo.size.width)
.frame(maxHeight: .infinity)
}
.padding(.horizontal)
}
}
}
}
Some other things I have tried:
- just using a normal scrollable textview and putting all my content in as text attachments in an NSAttributed string -> way too restrictive for my content and hard to detect user interactions
- setting anchor constraints on the textview in updateUIView() -> works great when the text is empty but if the screen loads with text in already then the layout of the parent view gets completely rearranged
- passing a binding from the parent to the textview for height & width like in How to use an NSAttributedString with a ScrollView in SwiftUI? -> doesn't work. I think the problems are less similar than they first appear - I want to set a fixed size for editable text whereas this question wants an adjustable content size for non-editable text
