Reputation: 828
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:
Upvotes: 7
Views: 1964
Reputation: 1
The code below solved the problem: UITextView (NSAttributedString) in ScrollView keeps changing the scroll position when the last line is typed (Solved, Remain a new problem)
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) {
let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: .infinity))
if textView.frame.size != size {
textView.frame.size = size
}
}
}
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
textView.translatesAutoresizingMaskIntoConstraints = false
textView.widthAnchor.constraint(equalToConstant: fixedWidth).isActive = true
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.attributedText = self.attributedString
}
}
Key method:
func textViewDidChange(_ textView: UITextView) {
let size = textView.sizeThatFits(CGSize(width:
textView.frame.size.width, height: .infinity))
if textView.frame.size != size {
textView.frame.size = size
}
}
Upvotes: 0
Reputation: 19004
Set width constraint to text view and set GeometryReader
at the top of the view.
The reason for the set GeometryReader
at top of, because inside the scroll view it will not allow to full scroll.
UIViewRepresentable Text View
struct TextView: UIViewRepresentable {
var attributedString: NSAttributedString
var fixedWidth: CGFloat
let textView = UITextView(frame: .zero)
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
}
func makeUIView(context: Context) -> UITextView {
textView.isEditable = true
textView.isScrollEnabled = false
textView.backgroundColor = .cyan // just to make it easier to see
textView.delegate = context.coordinator
textView.translatesAutoresizingMaskIntoConstraints = false
textView.widthAnchor.constraint(equalToConstant: fixedWidth).isActive = true //<--- Here
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
DispatchQueue.main.async { //<--- Here
textView.attributedText = self.attributedString
}
}
}
ContentView
struct ContentView: View {
@State private var textViewSize: CGSize = CGSize()
var body: some View {
GeometryReader { geo in //<--- Here
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0){
//Other content
Image(systemName: "photo")
.font(.largeTitle)
Button("A button"){
print("i'm a button")
}
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 screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is \n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new lineinstead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screen ------end"), fixedWidth: geo.size.width - 15 * 2) //<--- Here minus the padding for both size
}.padding(.horizontal, 15)
}
}
}
}
Upvotes: 10