UITextView que se expande para texto usando o layout automático

163

Eu tenho uma visão que é apresentada completamente usando o layout automático programaticamente. Eu tenho um UITextView no meio da exibição com itens acima e abaixo dele. Tudo funciona bem, mas quero poder expandir o UITextView à medida que o texto é adicionado. Isso deve empurrar tudo abaixo para baixo à medida que se expande.

Eu sei como fazer isso da maneira "molas e suportes", mas existe uma maneira de layout automático de fazer isso? A única maneira de pensar é removendo e adicionando novamente a restrição toda vez que ela precisar crescer.

tharris
fonte
5
Você pode criar um IBOutlet com uma restrição de altura para a visualização de texto e apenas modificar a constante quando quiser que ela cresça, sem a necessidade de remover e adicionar.
Rdelmar #
4
2017, uma dica enorme que muitas vezes te sustenta: FUNCIONA SOMENTE SE VOCÊ CONFIGURAR APÓS VIEWDID> APARECER < é muito frustrante se você definir o texto como em ViewDidLoad - não funcionará!
Fattie
2
Dica No2: se você chamar [someView.superView layoutIfNeeded], então ele pode trabalhar em viewWillAppear ();)
Yaro

Respostas:

30

A visualização que contém UITextView terá seu tamanho atribuído setBoundspelo AutoLayout. Então, foi o que eu fiz. A superview é inicialmente configurada como todas as outras restrições e, no final, coloquei uma restrição especial para a altura do UITextView e salvei-a em uma variável de instância.

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

[self addConstraint:_descriptionHeightConstraint];

No setBoundsmétodo, alterei o valor da constante.

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

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

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}
ancajic
fonte
4
Você também pode atualizar a restrição em 'textViewDidChange:' do UITextViewDelegate
LK__
2
Ótima solução. Observe que você também pode criar a restrição no Interface Builder e conectá-la usando uma tomada. Combinado com usando textViewDidChangeisso deve poupar algum aborrecimento subclasse e escrevendo código ...
Bob Vork
3
Lembre-se de que textViewDidChange:não será chamado ao adicionar texto ao UITextView programaticamente.
Wanderlust #
A sugestão do @BobVork funciona bem com o XCODE 11 / iOS13, define a restrição constante quando o valor da exibição de texto é definido em viewWillAppear e é atualizado no textViewDidChange.
ptc 7/02
549

Resumo: desative a rolagem da exibição de texto e não restrinja sua altura.

Para fazer isso programaticamente, insira o seguinte código em 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 fazer isso no Interface Builder, selecione a exibição de texto, desmarque Rolagem ativada no Inspetor de atributos e adicione as restrições manualmente.

Nota: Se você tiver outras visualizações acima / abaixo da visualização de texto, considere usar a UIStackViewpara organizar todas elas.

água vitaminada
fonte
12
Funciona para mim (iOS 7 @ xcode 5). A ressalva é que simplesmente desmarcar "rolagem ativada" no IB não ajudará. Você precisa fazer isso programaticamente.
9139 Kenneth Jiang
10
Como definir uma altura máxima na exibição de texto e fazer com que a exibição de texto role após o conteúdo ter ultrapassado a altura da exibição de texto?
ma11hew28
3
O @iOSGeek apenas cria uma subclasse UITextView e sobrescreve intrinsicContentSizedessa forma - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Diga-me se está funcionando para você. Não testei sozinho.
manuelwaldner
1
Defina essa propriedade e coloque UITextView no UIStackView. Voilla - auto redimensionamento textView)!
Nik Kov
3
Esta resposta é ouro. Com o Xcode 9, ele também funciona se você desabilitar a caixa de seleção no IB.
bio
48

Aqui está uma solução para pessoas que preferem fazer tudo por layout automático:

Inspetor de tamanho:

  1. Defina a prioridade da resistência à compactação de conteúdo na vertical para 1000.

  2. Diminua a prioridade da altura da restrição clicando em "Editar" em Restrições. Apenas faça menos que 1000.

insira a descrição da imagem aqui

No Inspetor de atributos:

  1. Desmarque a opção "Rolagem ativada"
Michael Revlis
fonte
Isso está funcionando perfeitamente. Eu não ajustei a altura. Só era necessário aumentar a prioridade da resistência à compressão vertical do UItextview para 1000. O UItextView possui algumas inserções, é por isso que o autolayout fornece erros, pois o UItextview precisará de pelo menos 32 alturas, mesmo se não houver conteúdo, quando a rolagem estiver desativada.
Nr5
Atualização: A restrição de altura é necessária se você deseja que a altura seja mínima para 20 ou qualquer outra coisa. Por outro lado, 32 será considerado como a altura mínima.
Nr5
@ Nil Fico feliz em ouvir isso ajuda.
Michael Revlis
@ MichaelRevlis, isso funciona muito bem. mas quando houver um texto grande, o texto não será exibido. você pode selecionar o texto para realizar outro processamento, mas ele não é visível. você pode me ajudar com isso? ? isso acontece quando desmarco a opção de rolagem ativada. quando faço a rolagem do textview ativar, ele é exibido perfeitamente, mas quero que a exibição do texto não role.
Bhautik Ziniya
@BhautikZiniya, você tentou criar seu aplicativo em um dispositivo real? Eu acho que pode ser um simulador exibindo bug. Eu tenho um UITextView com tamanho de fonte de texto 100. Ele é exibido bem em um dispositivo real, mas os textos são invisíveis em um simulador.
Michael Revlis
38

O UITextView não fornece um intrinsicContentSize, portanto você precisa subclassificá-lo e fornecer um. Para fazê-lo crescer automaticamente, invalide o intrinsicContentSize em layoutSubviews. Se você usar algo diferente do contentInset padrão (que eu não recomendo), pode ser necessário ajustar o 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
fonte
1
Isto funcionou bem para mim. Além disso, eu definiria 'scrollEnabled' como NO no Interface Builder para esta exibição de texto, pois ele já mostra todo o seu conteúdo sem a necessidade de rolagem. Mais importante, se estiver dentro de uma visualização de rolagem real, você poderá experimentar o salto do cursor perto da parte inferior da visualização de texto, se não desativar a rolagem na própria visualização de texto.
IdStar
Isso funciona bem. Curiosamente, ao substituir setScrollEnabled:para sempre configurá-lo, NOvocê obterá um loop infinito para um tamanho de conteúdo intrínseco cada vez maior.
22413 Pascal
No iOS 7.0, eu também precisava chamar [textView sizeToFit]o textViewDidChange:retorno de chamada delegado para o Auto Layout aumentar ou diminuir a exibição de texto (e recalcular qualquer restrição dependente) após cada pressionamento de tecla.
precisa saber é
6
Parece não ser mais necessário no iOS 7.1 Para corrigir e fornecer compatibilidade com versões anteriores, envolva a condição -(void)layoutSubviewse envolva todo o código -(CGSize)intrinsicContentSizecom version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
David James
1
@ DavidJames isso ainda é necessário no ios 7.1.2, não sei onde você está testando.
Softlion
28

Você pode fazer isso através do storyboard, basta desativar "Rolagem ativada" :)

StoryBoard

DaNLtR
fonte
2
A melhor resposta para esta pergunta
Ivan Byelko
Essa é a melhor solução quando você trabalha com o Layout automático.
JanApotheker
1
Configurações de layout automático (à esquerda, à direita, superior, inferior, SEM altura / largura ) + Rolagem desativada. e lá vai você. Obrigado!
Farhan Arshad
15

Descobri que não é totalmente incomum em situações em que você ainda pode precisar de isScrollEnabled definido como true para permitir uma interação razoável da interface do usuário. Um exemplo simples disso é quando você deseja permitir uma exibição de texto com expansão automática, mas ainda limita sua altura máxima a algo razoável em um UITableView.

Aqui está uma subclasse do UITextView que eu criei que permite expansão automática com layout automático, mas que você ainda pode restringir a uma altura máxima e que gerenciará se a exibição é rolável dependendo da altura. Por padrão, a exibição será expandida indefinidamente se você tiver suas restrições configuradas dessa maneira.

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 bônus, há suporte para incluir texto de espaço reservado semelhante ao UILabel.

Michael Link
fonte
7

Você também pode fazer isso sem subclassificar UITextView. Dê uma olhada na minha resposta para Como dimensionar um UITextView para seu conteúdo no iOS 7?

Use o valor desta expressão:

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

para atualizar o constantda textViewaltura 's UILayoutConstraint.

ma11hew28
fonte
3
Onde você está fazendo isso e mais alguma coisa para garantir que o UITextView esteja configurado? Estou fazendo isso e o tamanho da exibição de texto é dimensionado, mas o próprio texto é cortado na metade da altura do UITextView. O texto é definido programaticamente logo antes de fazer as coisas acima.
precisa
Achei isso mais útil do que o truque scrollEnabled = NO, se você quiser definir a largura manualmente.
jchnxu
3

Uma coisa importante a ser observada:

Como UITextView é uma subclasse de UIScrollView, ela está sujeita à propriedade automaticamenteAdjustsScrollViewInsets de UIViewController.

Se você estiver configurando o layout e o TextView for a primeira subvisão na hierarquia UIViewControllers, ele terá seus contentInsets modificados se a opção AutomaticAdjustsScrollViewInsets for verdadeira, algumas vezes causando comportamento inesperado no layout automático.

Portanto, se você estiver tendo problemas com o layout automático e as visualizações de texto, tente definir automaticallyAdjustsScrollViewInsets = falseo controlador de exibição ou mover o textView para frente na hierarquia.

DaveIngle
fonte
2

Este comentário mais importante

A chave para entender por que a resposta da vitaminwater funciona são três coisas:

  1. Saiba que UITextView é uma subclasse da classe UIScrollView
  2. Entenda como o ScrollView funciona e como seu contentSize é calculado. Para mais informações, veja aqui a resposta e suas várias soluções e comentários.
  3. Entenda o que é contentSize e como é calculado. Veja aqui e aqui . Ele também pode ajudar a essa definição contentOffseté provavelmente nada, mas:

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

Para obter mais informações, consulte objc scrollview e compreendendo scrollview


Ao combinar os três, você entenderia facilmente que precisa permitir que o contentSize intrínseco do textView funcione de acordo com as restrições de AutoLayout do textView para conduzir a lógica. É quase como se seu textView estivesse funcionando como um UILabel

Para que isso aconteça, você precisa desativar a rolagem, que basicamente significa o tamanho do scrollView, o tamanho do contentSize e, no caso de adicionar um containerView, o tamanho do containerView será o mesmo. Quando são iguais, você NÃO tem rolagem. E você teria . Ter significa que você não rolou para baixo. Nem um ponto a menos! Como resultado, o textView será todo estendido.0 contentOffset0 contentOffSet

Também não vale nada, o que significa que os limites e o quadro do scrollView são idênticos. Se você rolar 5 pontos para baixo, seu contentOffset seria , enquanto seu seria igual a0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5

Mel
fonte
1

Solução Plug and Play - Xcode 9

Execução automática como UILabel, com a detecção de links , seleção de texto , edição e rolagem de UITextView.

Lida automaticamente

  • Área segura
  • Inserções de conteúdo
  • Preenchimento de fragmento de linha
  • Inserções de contêiner de texto
  • Restrições
  • Visualizações de pilha
  • Sequências atribuídas
  • Tanto faz.

Muitas dessas respostas me deixaram 90% lá, mas nenhuma foi à prova de idiotas.

Entre nesta UITextViewsubclasse e você é bom.


#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]));
}
beebcon
fonte
0

A resposta da vitaminwater está funcionando para mim.

Se o texto da sua visualização de texto estiver saltando para cima e para baixo durante a edição, após a configuração [textView setScrollEnabled:NO];, defina Size Inspector > Scroll View > Content Insets > Never.

Espero que ajude.

Barão Ch'ng
fonte
0

Coloque o UILabel oculto embaixo da sua visualização de texto. Linhas de rótulo = 0. Defina as restrições do UITextView como iguais ao UILabel (centerX, centerY, largura, altura). Funciona mesmo se você deixar o comportamento de rolagem do textView.

Yuriy
fonte
-1

BTW, criei um UITextView em expansão usando uma subclasse e substituindo o tamanho do conteúdo intrínseco. Descobri um bug no UITextView que você pode querer investigar em sua própria implementação. Aqui está o problema:

A exibição de texto em expansão aumentaria para acomodar o texto em crescimento se você digitar letras únicas por vez. Mas se você colar um monte de texto nele, ele não diminuirá, mas o texto rolará para cima e o texto na parte superior ficará fora de vista.

A solução: Substituir setBounds: na sua subclasse. Por alguma razão desconhecida, a colagem fez com que o valor bounds.origin.y não fosse zee (33 em todos os casos que vi). Então, eu substitui setBounds: para sempre definir o bounds.origin.y como zero. Corrigido o problema.

Alan McKean
fonte
-1

Aqui está uma solução rápida:

Esse problema pode ocorrer se você tiver definido a propriedade clipsToBounds como false na sua visualização de texto. Se você simplesmente excluí-lo, o problema desaparece.

myTextView.clipsToBounds = false //delete this line
Eray Alparslan
fonte
-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
fonte