2

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:

Proper layout

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:

Wrapped but wrong height

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:

No longer wrapping

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?

jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • This topic [How do I create a multiline TextField in SwiftUI?](https://stackoverflow.com/questions/56471973/how-do-i-create-a-multiline-textfield-in-swiftui/58639072#58639072) should be helpful. – Asperi Apr 02 '20 at 14:39
  • Thanks for the link @Asperi, I was hoping there was a way to do it without manual frame calculations but it's good to know that it's at least possible that way! – jjoelson Apr 02 '20 at 15:09

1 Answers1

0

Try using .fixedSize(horizontal: false, vertical: true)

or setting the UITextView's content hugging priority to required on the vertical axis.

Tested this on iOS 16 and it seems to work. Not sure about previous OSes

Xaxxus
  • 1,083
  • 12
  • 19