UITextView que se expande a texto usando el diseño automático

163

Tengo una vista que se presenta completamente usando el diseño automático mediante programación. Tengo un UITextView en el medio de la vista con elementos arriba y abajo. Todo funciona bien, pero quiero poder expandir UITextView a medida que se agrega texto. Esto debería empujar todo debajo de él a medida que se expande.

Sé cómo hacer esto a la manera de "resortes y puntales", pero ¿hay una forma de diseño automático para hacerlo? La única forma en que puedo pensar es eliminando y volviendo a agregar la restricción cada vez que necesita crecer.

tharris
fuente
55
Puede convertir un IBOutlet en una restricción de altura para la vista de texto y simplemente modificar la constante cuando desee que crezca, sin necesidad de eliminar y agregar.
rdelmar
44
2017, un gran consejo que a menudo te detiene: SOLO FUNCIONA SI LO CONFIGURAS DESPUÉS DE VIEWDID> APARECER < es muy frustrante si configuras el texto en ViewDidLoad, ¡no funcionará!
Fattie
2
consejo No2: si llama a [someView.superView layoutIfNeeded] ENTONCES podría funcionar en viewWillAppear ();)
Yaro

Respuestas:

30

AutoLayout le asignará su tamaño a la vista que contiene UITextView setBounds. Entonces, esto es lo que hice. La supervista inicialmente configura todas las demás restricciones como deberían ser, y al final puse una restricción especial para la altura de UITextView, y la guardé en una variable de instancia.

_descriptionHeightConstraint = [NSLayoutConstraint constraintWithItem:_descriptionTextView
                                 attribute:NSLayoutAttributeHeight 
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:nil 
                                 attribute:NSLayoutAttributeNotAnAttribute 
                                multiplier:0.f 
                                 constant:100];

[self addConstraint:_descriptionHeightConstraint];

En el setBoundsmétodo, luego cambié el valor de la constante.

-(void) setBounds:(CGRect)bounds
{
    [super setBounds:bounds];

    _descriptionTextView.frame = bounds;
    CGSize descriptionSize = _descriptionTextView.contentSize;

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}
ancajic
fuente
44
También puede actualizar la restricción en 'textViewDidChange:' del UITextViewDelegate
LK__
2
Buena solución Tenga en cuenta que también puede crear la restricción en Interface Builder y conectarla mediante una salida. Combinado con el uso de textViewDidChangeesto debería ahorrar algunas subclases de problemas y escribir código ...
Bob Vork
3
Tenga en cuenta que textViewDidChange:no se llamará al agregar texto al UITextView mediante programación.
wanderlust
La sugerencia de @BobVork funciona bien XCODE 11 / iOS13, establezca la restricción constante cuando el valor de vista de texto establecido en viewWillAppear y actualice en textViewDidChange.
ptc
549

Resumen: deshabilite el desplazamiento de su vista de texto y no restrinja su altura.

Para hacer esto mediante programación, ingrese el siguiente código viewDidLoad:

let textView = UITextView(frame: .zero, textContainer: nil)
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false   // causes expanding height
view.addSubview(textView)

// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
    textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
    textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
    textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])

Para hacer esto en Interface Builder, seleccione la vista de texto, desmarque Desplazamiento habilitado en el Inspector de atributos y agregue las restricciones manualmente.

Nota: Si tiene otras vistas arriba / abajo de su vista de texto, considere usar a UIStackViewpara organizarlas todas.

agua vitaminada
fuente
12
Funciona para mí (iOS 7 @ xcode 5). La advertencia es que simplemente desmarcar "desplazamiento habilitado" en IB no ayudará. Tienes que hacerlo programáticamente.
Kenneth Jiang
10
¿Cómo configuro una altura máxima en la vista de texto y luego hago que la vista de texto se desplace una vez que su contenido ha superado la altura de la vista de texto?
ma11hew28
3
@iOSGeek solo crea una subclase UITextView y sobrescribe de intrinsicContentSizeesta manera - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Dime si te está funcionando. No lo probé yo mismo.
manuelwaldner
1
Establezca esta propiedad y coloque UITextView en UIStackView. Voilla - textView de cambio de tamaño automático)
Nik Kov
3
Esta respuesta es oro. Con Xcode 9 también funciona si deshabilita la casilla de verificación en IB.
bio
48

Aquí hay una solución para las personas que prefieren hacerlo todo mediante el diseño automático:

En tamaño Inspector:

  1. Establezca la prioridad de resistencia de compresión de contenido vertical en 1000.

  2. Reduzca la prioridad de la altura de restricción haciendo clic en "Editar" en Restricciones. Solo hazlo menos de 1000.

ingrese la descripción de la imagen aquí

En el inspector de atributos:

  1. Desmarca "Desplazamiento habilitado"
Michael Revlis
fuente
Esto está funcionando perfectamente. No puse la altura. Solo se requería aumentar la prioridad de la resistencia de compresión vertical de UItextview a 1000. El UItextView tiene algunas inserciones, es por eso que el autolayout produce errores ya que UItextview necesitará al menos 32 de altura, incluso si no hay contenido, cuando el desplazamiento está deshabilitado.
nr5
Actualización: se requiere restricción de altura si desea que la altura sea mínima de 20 o cualquier otra cosa. Otros sabios 32 serán considerados como la altura mínima.
nr5
@Nil Me alegra saber que ayuda.
Michael Revlis
@ MichaelRevlis, esto funciona muy bien. pero cuando hay un texto grande, el texto no se muestra. puede seleccionar el texto para realizar otro procesamiento pero no es visible. ¿Puedes ayudarme con eso? ? sucede cuando desactivo la opción de desplazamiento habilitado. cuando habilito el desplazamiento de vista de texto, se muestra perfectamente pero quiero que la vista de texto no se desplace.
Bhautik Ziniya
@BhautikZiniya, ¿has intentado construir tu aplicación en un dispositivo real? Creo que podría ser un simulador que muestra un error. Tengo un UITextView con tamaño de fuente de texto 100. Se muestra bien en un dispositivo real pero los textos son invisibles en un simulador.
Michael Revlis
38

UITextView no proporciona un tamaño de contenido intrínseco, por lo que debe subclasificarlo y proporcionar uno. Para que crezca automáticamente, invalide intrinsicContentSize en layoutSubviews. Si usa algo que no sea el contentInset predeterminado (que no recomiendo), es posible que deba ajustar el cálculo intrinsicContentSize.

@interface AutoTextView : UITextView

@end

#import "AutoTextView.h"

@implementation AutoTextView

- (void) layoutSubviews
{
    [super layoutSubviews];

    if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) {
        [self invalidateIntrinsicContentSize];
    }
}

- (CGSize)intrinsicContentSize
{
    CGSize intrinsicContentSize = self.contentSize;

    // iOS 7.0+
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f) {
        intrinsicContentSize.width += (self.textContainerInset.left + self.textContainerInset.right ) / 2.0f;
        intrinsicContentSize.height += (self.textContainerInset.top + self.textContainerInset.bottom) / 2.0f;
    }

    return intrinsicContentSize;
}

@end
dhallman
fuente
1
Esto funcionó bien para mí. Además, establecería 'scrollEnabled' en NO en Interface Builder para esta Vista de texto, ya que ya muestra todo su contenido sin necesidad de desplazamiento. Más importante aún, si se encuentra dentro de una vista de desplazamiento real, puede experimentar el salto del cursor cerca de la parte inferior de la vista de texto, si no deshabilita el desplazamiento en la vista de texto en sí.
idStar
Esto funciona muy bien. Curiosamente, cuando se anula setScrollEnabled:para configurarlo siempre NO, obtendrá un bucle infinito para un tamaño de contenido intrínseco cada vez mayor.
Pascal
En iOS 7.0, también necesitaba llamar [textView sizeToFit]a la textViewDidChange:devolución de llamada delegada para que Auto Layout aumentara o redujera la vista de texto (y recalculara cualquier restricción dependiente) después de cada pulsación de tecla.
Seguido
66
Parece que ya no es necesario en iOS 7.1. Para arreglar y proporcionar compatibilidad con versiones anteriores, envuelva la condición -(void)layoutSubviewsy envuelva todo el código -(CGSize)intrinsicContentSizecon version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
David James
1
@DavidJames, esto todavía es necesario en iOS 7.1.2, no sé dónde está probando.
Softlion
28

Puede hacerlo a través del guión gráfico, simplemente deshabilite "Desplazamiento habilitado" :)

StoryBoard

DaNLtR
fuente
2
La mejor respuesta a esta pregunta
Ivan Byelko
Esta es la mejor solución cuando trabaja con Auto Layout.
JanApotheker
1
Configuración de diseño automático (anterior, posterior, superior, inferior, NO altura / anchura ) + Desplazamiento desactivado. y ahí tienes ¡Gracias!
Farhan Arshad
15

He descubierto que no es del todo raro en situaciones en las que aún pueda necesitar isScrollEnabled establecido en verdadero para permitir una interacción razonable de la interfaz de usuario. Un caso simple para esto es cuando desea permitir una vista de texto de expansión automática pero aún así limitar su altura máxima a algo razonable en un UITableView.

Aquí hay una subclase de UITextView que he creado que permite la expansión automática con diseño automático, pero que aún puede restringir a una altura máxima y que administrará si la vista es desplazable según la altura. Por defecto, la vista se expandirá indefinidamente si tiene sus restricciones configuradas de esa manera.

import UIKit

class FlexibleTextView: UITextView {
    // limit the height of expansion per intrinsicContentSize
    var maxHeight: CGFloat = 0.0
    private let placeholderTextView: UITextView = {
        let tv = UITextView()

        tv.translatesAutoresizingMaskIntoConstraints = false
        tv.backgroundColor = .clear
        tv.isScrollEnabled = false
        tv.textColor = .disabledTextColor
        tv.isUserInteractionEnabled = false
        return tv
    }()
    var placeholder: String? {
        get {
            return placeholderTextView.text
        }
        set {
            placeholderTextView.text = newValue
        }
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        isScrollEnabled = false
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: Notification.Name.UITextViewTextDidChange, object: self)
        placeholderTextView.font = font
        addSubview(placeholderTextView)

        NSLayoutConstraint.activate([
            placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
            placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
            placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
            placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var text: String! {
        didSet {
            invalidateIntrinsicContentSize()
            placeholderTextView.isHidden = !text.isEmpty
        }
    }

    override var font: UIFont? {
        didSet {
            placeholderTextView.font = font
            invalidateIntrinsicContentSize()
        }
    }

    override var contentInset: UIEdgeInsets {
        didSet {
            placeholderTextView.contentInset = contentInset
        }
    }

    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize

        if size.height == UIViewNoIntrinsicMetric {
            // force layout
            layoutManager.glyphRange(for: textContainer)
            size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
        }

        if maxHeight > 0.0 && size.height > maxHeight {
            size.height = maxHeight

            if !isScrollEnabled {
                isScrollEnabled = true
            }
        } else if isScrollEnabled {
            isScrollEnabled = false
        }

        return size
    }

    @objc private func textDidChange(_ note: Notification) {
        // needed incase isScrollEnabled is set to true which stops automatically calling invalidateIntrinsicContentSize()
        invalidateIntrinsicContentSize()
        placeholderTextView.isHidden = !text.isEmpty
    }
}

Como beneficio adicional, hay soporte para incluir texto de marcador de posición similar a UILabel.

Michael Link
fuente
7

También puedes hacerlo sin subclasificar UITextView. Eche un vistazo a mi respuesta a ¿Cómo dimensiono un UITextView a su contenido en iOS 7?

Usa el valor de esta expresión:

[textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height

para actualizar el constantde la textViewaltura 's UILayoutConstraint.

ma11hew28
fuente
3
¿Dónde está haciendo esto y está haciendo algo más para asegurarse de que UITextView esté configurado? Estoy haciendo esto, y la vista de texto en sí se ajusta, pero el texto en sí se corta a la mitad de la altura de UITextView. El texto se establece mediante programación justo antes de hacer las cosas anteriores.
chadbag
Encontré esto más generalmente útil que el truco scrollEnabled = NO, si desea establecer el ancho manualmente.
jchnxu
3

Una cosa importante a tener en cuenta:

Dado que UITextView es una subclase de UIScrollView, está sujeto a la propiedad automaticAdjustsScrollViewInsets de UIViewController.

Si está configurando el diseño y TextView es la primera subvista en una jerarquía UIViewControllers, tendrá sus contentInsets modificados si automaticAdjustsScrollViewInsets es verdadero, a veces causando un comportamiento inesperado en el diseño automático.

Entonces, si tiene problemas con el diseño automático y las vistas de texto, intente configurar automaticallyAdjustsScrollViewInsets = falseel controlador de vista o mover textView hacia adelante en la jerarquía.

DaveIngle
fuente
2

Este es un comentario muy importante

La clave para entender por qué funciona la respuesta de vitaminwater son tres cosas:

  1. Sepa que UITextView es una subclase de la clase UIScrollView
  2. Comprenda cómo funciona ScrollView y cómo se calcula su contentSize. Para más ver esto aquí respuesta y sus diversas soluciones y comentarios.
  3. Comprenda qué es contentSize y cómo se calcula. Mira aquí y aquí . También podría ayudar a que el ajuste contentOffsetes probable que nada más que:

func setContentOffset(offset: CGPoint)
{
    CGRect bounds = self.bounds
    bounds.origin = offset
    self.bounds = bounds
}

Para más información, vea objc scrollview y comprenda scrollview


Combinando los tres juntos, comprendería fácilmente que necesita permitir que el contentSize intrínseco de textView funcione junto con las restricciones de AutoLayout de textView para impulsar la lógica. Es casi como si tu textView funciona como un UILabel

Para que eso suceda, debe deshabilitar el desplazamiento, que básicamente significa el tamaño de scrollView, el tamaño de contentSize y, en caso de agregar un containerView, entonces el tamaño de containerView sería el mismo. Cuando son lo mismo, NO tienes desplazamiento. Y lo habrías hecho . Tener significa que no te has desplazado hacia abajo. ¡Ni siquiera un punto por debajo! Como resultado, la vista de texto se extenderá.0 contentOffset0 contentOffSet

Tampoco vale nada, lo que significa que los límites y el marco de scrollView son idénticos. Si se desplaza hacia abajo 5 puntos, entonces su contentOffset sería , mientras que su sería igual a0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5

Miel
fuente
1

Solución Plug and Play - Xcode 9

Autolayout al igual que UILabelcon la detección de enlaces , selección de texto , edición y desplazamiento de UITextView.

Maneja automáticamente

  • Area segura
  • Inserciones de contenido
  • Relleno de fragmentos de línea
  • Inserciones de contenedores de texto
  • Restricciones
  • Vistas de pila
  • Cadenas atribuidas
  • Lo que sea.

Muchas de estas respuestas me dieron un 90%, pero ninguna fue infalible.

Entra en esta UITextViewsubclase y eres bueno.


#pragma mark - Init

- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer
{
    self = [super initWithFrame:frame textContainer:textContainer];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit
{
    // Try to use max width, like UILabel
    [self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
    
    // Optional -- Enable / disable scroll & edit ability
    self.editable = YES;
    self.scrollEnabled = YES;
    
    // Optional -- match padding of UILabel
    self.textContainer.lineFragmentPadding = 0.0;
    self.textContainerInset = UIEdgeInsetsZero;
    
    // Optional -- for selecting text and links
    self.selectable = YES;
    self.dataDetectorTypes = UIDataDetectorTypeLink | UIDataDetectorTypePhoneNumber | UIDataDetectorTypeAddress;
}

#pragma mark - Layout

- (CGFloat)widthPadding
{
    CGFloat extraWidth = self.textContainer.lineFragmentPadding * 2.0;
    extraWidth +=  self.textContainerInset.left + self.textContainerInset.right;
    if (@available(iOS 11.0, *)) {
        extraWidth += self.adjustedContentInset.left + self.adjustedContentInset.right;
    } else {
        extraWidth += self.contentInset.left + self.contentInset.right;
    }
    return extraWidth;
}

- (CGFloat)heightPadding
{
    CGFloat extraHeight = self.textContainerInset.top + self.textContainerInset.bottom;
    if (@available(iOS 11.0, *)) {
        extraHeight += self.adjustedContentInset.top + self.adjustedContentInset.bottom;
    } else {
        extraHeight += self.contentInset.top + self.contentInset.bottom;
    }
    return extraHeight;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    // Prevents flashing of frame change
    if (CGSizeEqualToSize(self.bounds.size, self.intrinsicContentSize) == NO) {
        [self invalidateIntrinsicContentSize];
    }
    
    // Fix offset error from insets & safe area
    
    CGFloat textWidth = self.bounds.size.width - [self widthPadding];
    CGFloat textHeight = self.bounds.size.height - [self heightPadding];
    if (self.contentSize.width <= textWidth && self.contentSize.height <= textHeight) {
        
        CGPoint offset = CGPointMake(-self.contentInset.left, -self.contentInset.top);
        if (@available(iOS 11.0, *)) {
            offset = CGPointMake(-self.adjustedContentInset.left, -self.adjustedContentInset.top);
        }
        if (CGPointEqualToPoint(self.contentOffset, offset) == NO) {
            self.contentOffset = offset;
        }
    }
}

- (CGSize)intrinsicContentSize
{
    if (self.attributedText.length == 0) {
        return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
    }
    
    CGRect rect = [self.attributedText boundingRectWithSize:CGSizeMake(self.bounds.size.width - [self widthPadding], CGFLOAT_MAX)
                                                    options:NSStringDrawingUsesLineFragmentOrigin
                                                    context:nil];
    
    return CGSizeMake(ceil(rect.size.width + [self widthPadding]),
                      ceil(rect.size.height + [self heightPadding]));
}
abeja
fuente
0

La respuesta de vitaminwater está funcionando para mí.

Si el texto de su vista de texto rebota hacia arriba y hacia abajo durante la edición, después de configurar [textView setScrollEnabled:NO];, configure Size Inspector > Scroll View > Content Insets > Never.

Espero eso ayude.

Baron Ch'ng
fuente
0

Coloque UILabel oculto debajo de su vista de texto. Etiquetar líneas = 0. Establecer restricciones de UITextView para que sean iguales a UILabel (centerX, centerY, width, height). Funciona incluso si deja el comportamiento de desplazamiento de textView.

Yuriy
fuente
-1

Por cierto, construí un UITextView en expansión utilizando una subclase y anulando el tamaño del contenido intrínseco. Descubrí un error en UITextView que es posible que desee investigar en su propia implementación. Aquí está el problema:

La vista de texto desplegable crecería hacia abajo para acomodar el texto creciente si escribe letras individuales a la vez. Pero si pega un montón de texto en él, no crecerá hacia abajo, pero el texto se desplazará hacia arriba y el texto en la parte superior estará fuera de la vista.

La solución: anular setBounds: en su subclase. Por alguna razón desconocida, el pegado causó que el valor de fields.origin.y no sea zee (33 en todos los casos que vi). Por lo tanto, anulé setBounds: para establecer siempre los fields.origin.y en cero. Solucionado el problema.

Alan McKean
fuente
-1

Aquí hay una solución rápida:

Este problema puede ocurrir si ha establecido la propiedad clipsToBounds en false de su vista de texto. Si simplemente lo elimina, el problema desaparece.

myTextView.clipsToBounds = false //delete this line
Eray Alparslan
fuente
-3
Obj C:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic) UITextView *textView;
@end



#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize textView;

- (void)viewDidLoad{
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor grayColor]];
    self.textView = [[UITextView alloc] initWithFrame:CGRectMake(30,10,250,20)];
    self.textView.delegate = self;
    [self.view addSubview:self.textView];
}

- (void)didReceiveMemoryWarning{
    [super didReceiveMemoryWarning];
}

- (void)textViewDidChange:(UITextView *)txtView{
    float height = txtView.contentSize.height;
    [UITextView beginAnimations:nil context:nil];
    [UITextView setAnimationDuration:0.5];

    CGRect frame = txtView.frame;
    frame.size.height = height + 10.0; //Give it some padding
    txtView.frame = frame;
    [UITextView commitAnimations];
}

@end
AG
fuente