UITextView: ajuste el tamaño según el contenido en SwiftUI

8

Estoy tratando de descubrir cómo hacer que el tamaño de UITextView dependa de su contenido en SwiftUI. Envolví el UITextView de la UIViewRepresentablesiguiente manera:

struct TextView: UIViewRepresentable {

    @Binding var showActionSheet: Bool

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {

        let uiTextView = UITextView()
        uiTextView.delegate = context.coordinator

        uiTextView.font = UIFont(name: "HelveticaNeue", size: 15)
        uiTextView.isScrollEnabled = true
        uiTextView.isEditable = true
        uiTextView.isUserInteractionEnabled = true
        uiTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
        uiTextView.isEditable = false

        return uiTextView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = prepareText(question: question)

        var frame = uiView.frame
        frame.size.height = uiView.contentSize.height
        uiView.frame = frame
    }

    func prepareText(text: string) -> NSMutableAttributedString {
        ...................
        return attributedText
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ view: TextView) {
            self.parent = view
        }

        func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
            parent.showActionSheet = true
            return false
        }
    }
}

Además, intenté cambiar el tamaño del marco en updateUIView en función de su tamaño de contenido, pero no tuvo ningún efecto. Supongo que en esta etapa, la vista ni siquiera es un diseño y su marco se anula en otro lugar. Realmente agradecería si alguien pudiera señalarme en la dirección correcta.

Adán
fuente

Respuestas:

3

Pude hacer que esto funcione al vincular una variable en TextView que usa la vista de encapsulación para establecer la altura del marco. Aquí está la implementación mínima de TextView:

struct TextView: UIViewRepresentable {

    @Binding var text: String?
    @Binding var attributedText: NSAttributedString?
    @Binding var desiredHeight: CGFloat

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {

        let uiTextView = UITextView()
        uiTextView.delegate = context.coordinator

        // Configure text view as desired...
        uiTextView.font = UIFont(name: "HelveticaNeue", size: 15)

        return uiTextView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        if self.attributedText != nil {
            uiView.attributedText = self.attributedText
        } else {
            uiView.text = self.attributedText
        }

        // Compute the desired height for the content
        let fixedWidth = uiView.frame.size.width
        let newSize = uiView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))

        DispatchQueue.main.async {
            self.desiredHeight = newSize.height
        }
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ view: TextView) {
            self.parent = view
        }

        func textViewDidEndEditing(_ textView: UITextView) {
            DispatchQueue.main.async {
                self.parent.text = textView.text
                self.parent.attributedText = textView.attributedText
            }
        }
    }
}

La clave es el enlace de deseadoHeight, que se calcula en updateUIView utilizando el método UIView sizeThatFits. Tenga en cuenta que esto está envuelto en un bloque DispatchQueue.main.async para evitar el error SwiftUI "Modificación del estado durante la actualización de la vista".

Ahora puedo usar esta vista en mi ContentView:

struct ContentView: View {

    @State private var notes: [String?] = [
        "This is a short Note",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet consectetur. Morbi enim nunc faucibus a. Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero.",
    ]

    @State private var desiredHeight: [CGFloat] = [0, 0]

    var body: some View {
        List {
            ForEach(0..<notes.count, id: \.self) { index in
                TextView(
                    desiredHeight: self.$desiredHeight[index],
                    text: self.$notes[index],
                    attributedText: .constant(nil)
                )
                .frame(height: max(self.desiredHeight[index], 100))
            }
        }
    }
}

Aquí tengo un par de notas en una matriz de cadenas, junto con una matriz de valores de altura deseados para vincular a TextView. La altura de TextView se establece en el modificador de marco en TextView. En este ejemplo, también establecí una altura mínima para dar algo de espacio para la edición inicial. La altura del cuadro solo se actualiza cuando cambia uno de los valores de Estado (en este caso, las notas). En la implementación de TextView aquí, esto solo ocurre cuando finaliza la edición en la vista de texto.

Intenté actualizar el texto en la función de delegado textViewDidChange en el Coordinador. Esto actualiza la altura del marco a medida que agrega texto, pero hace que solo pueda escribir texto al final de TextView, ya que la actualización de la vista restablece el punto de inserción al final.

RocketDoc
fuente