UITextView che si espande nel testo usando il layout automatico


163

Ho una vista che è completamente impostata utilizzando il layout automatico a livello di codice. Ho un UITextView nel mezzo della vista con elementi sopra e sotto di esso. Tutto funziona bene, ma voglio essere in grado di espandere UITextView man mano che il testo viene aggiunto. Questo dovrebbe spingere tutto sotto di esso mentre si espande.

So come farlo nel modo "molle e puntoni", ma esiste un modo di layout automatico per farlo? L'unico modo in cui riesco a pensare è rimuovere e aggiungere nuovamente il vincolo ogni volta che deve crescere.


5
È possibile creare un IBOutlet a un vincolo di altezza per la vista di testo e modificare semplicemente la costante quando si desidera che cresca, non è necessario rimuovere e aggiungere.
rdelmar,

4
2017, un grande consiglio che spesso ti regge: FUNZIONA SOLO SE LO IMPOSTISCI DOPO VIEWDID> APPEAR < è molto frustrante se imposti il ​​testo su ViewDidLoad: non funzionerà!
Fattie,

2
suggerimento No2: se si chiama [someView.superView layoutIfNeeded] POI potrebbe funzionare in viewWillAppear ();)
Yaro

Risposte:


30

La visualizzazione contenente UITextView verrà assegnata alle sue dimensioni setBoundsda AutoLayout. Quindi, questo è quello che ho fatto. Inizialmente, la superview imposta tutti gli altri vincoli come dovrebbero essere e alla fine ho inserito un vincolo speciale per l'altezza di UITextView e l'ho salvato in una variabile di istanza.

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

[self addConstraint:_descriptionHeightConstraint];

Nel setBoundsmetodo, ho quindi modificato il valore della costante.

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

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

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}

4
Puoi anche aggiornare il vincolo in 'textViewDidChange:' di UITextViewDelegate
LK__

2
Bella soluzione. Si noti che è anche possibile creare il vincolo in Interface Builder e collegarlo tramite una presa. In combinazione con l'utilizzo di textViewDidChangequesto dovrebbe salvare un po 'di sottoclasse e scrivere codice ...
Bob Vork,

3
Tenere presente che textViewDidChange:non verrà chiamato quando si aggiunge testo a UITextView a livello di codice.
Wanderlust

Il suggerimento di @BobVork funziona bene XCODE 11 / iOS13, imposta la costante di vincolo quando il valore della vista di testo è impostato in viewWillAppear e aggiorna in textViewDidChange.
ptc

549

Riepilogo: disabilita lo scorrimento della visualizzazione del testo e non vincolarne l'altezza.

Per fare ciò a livello di codice, inserisci il seguente codice 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)
])

Per fare ciò in Interface Builder, seleziona la vista di testo, deseleziona Scorrimento abilitato nella finestra di ispezione degli attributi e aggiungi i vincoli manualmente.

Nota: se hai altre viste sopra / sotto la vista di testo, considera l'utilizzo di a UIStackViewper disporli tutti.


12
Funziona per me (iOS 7 @ xcode 5). L'avvertenza è che semplicemente deselezionare "scorrimento abilitato" in IB non aiuta. Devi farlo a livello di programmazione.
Kenneth Jiang,

10
Come posso impostare un'altezza massima nella vista testo e quindi far scorrere la vista testo dopo che il suo contenuto ha superato l'altezza della vista testo?
ma11hew28,

3
@iOSGeek crea semplicemente una sottoclasse UITextView e sovrascrivi in intrinsicContentSizequesto modo - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Dimmi se funziona per te. Non l'ho provato io stesso.
manuelwaldner,

1
Imposta questa proprietà e inserisci UITextView in UIStackView. Voilla - auto-ridimensionamento textView!)
Nik Kov

3
Questa risposta è oro. Con Xcode 9 funziona anche se si disabilita la casella di controllo in IB.
bio

48

Ecco una soluzione per le persone che preferiscono fare tutto con il layout automatico:

In Ispettore dimensioni:

  1. Impostare la priorità della resistenza di compressione del contenuto su 1000.

  2. Ridurre la priorità dell'altezza del vincolo facendo clic su "Modifica" in Vincoli. Fallo solo meno di 1000.

inserisci qui la descrizione dell'immagine

In Impostazioni attributi:

  1. Deseleziona "Scorrimento abilitato"

Funziona perfettamente. Non ho impostato l'altezza. L'aumento della priorità della resistenza alla compressione verticale di UItextview a 1000 era solo necessario. UItextView ha alcuni inserti, ecco perché autolayout genera errori poiché UItextview avrà bisogno di almeno 32 altezza, anche se non è presente alcun contenuto, quando lo scorrimento è disabilitato.
nr5

Aggiornamento: il vincolo di altezza è richiesto se si desidera che l'altezza sia almeno 20 o altro. Altri saggi 32 saranno considerati l'altezza minima.
nr5

@Nil Sono contento di sentire che aiuta.
Michael Revlis,

@MichaelRevlis, Funziona benissimo. ma quando c'è un testo grande, il testo non viene visualizzato. è possibile selezionare il testo per eseguire altre elaborazioni ma non è visibile. puoi aiutarmi per favore con quello. ? succede quando deseleziono l'opzione di scorrimento abilitato. quando abilito lo scorrimento della visualizzazione del testo, viene visualizzato perfettamente ma voglio che la vista del testo non scorra.
Bhautik Ziniya

@BhautikZiniya, hai provato a creare la tua app su un dispositivo reale? Penso che potrebbe essere un simulatore che mostra un bug. Ho un UITextView con dimensione del carattere del testo 100. Viene visualizzato bene su un dispositivo reale ma i testi sono invisibili su un simulatore.
Michael Revlis,

38

UITextView non fornisce intrinsicContentSize, quindi è necessario sottoclassarlo e fornirne uno. Per farlo crescere automaticamente, invalidare intrinsicContentSize in layoutSubviews. Se si utilizza qualcosa di diverso da contentInset predefinito (che non è consigliabile), potrebbe essere necessario modificare il calcolo 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

1
Questo ha funzionato bene per me. Inoltre, impostare 'scrollEnabled' su NO in Interface Builder per questa vista di testo, poiché mostra già tutto il suo contenuto senza scorrere necessario. Ancora più importante, se si trova all'interno di una vista di scorrimento effettiva, è possibile che il cursore salti vicino alla parte inferiore della vista di testo, se non si disabilita lo scorrimento sulla vista di testo stessa.
idStar,

Funziona bene. È interessante notare che quando si esegue setScrollEnabled:l' override per impostarlo sempre su, NOsi otterrà un ciclo infinito per una dimensione di contenuto intrinseco in continua crescita.
Pascal,

Su iOS 7.0, avevo anche bisogno di chiamare [textView sizeToFit]il textViewDidChange:callback delegato per Auto Layout per aumentare o ridurre la visualizzazione del testo (e ricalcolare eventuali vincoli dipendenti) dopo ogni sequenza di tasti.
followben,

6
Questo sembra non essere più necessario in iOS 7.1 Per correggere e fornire la compatibilità all'indietro, avvolgi la condizione -(void)layoutSubviewse racchiudi tutto il codice -(CGSize)intrinsicContentSizecon version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
David James

1
@DavidJames è ancora necessario in iOS 7.1.2, non so dove stai testando.
Softlion,

28

Puoi farlo tramite lo storyboard, basta disabilitare "Scorrimento abilitato" :)

StoryBoard


2
La migliore risposta a questa domanda
Ivan Byelko il

Questa è la soluzione migliore quando lavori con il layout automatico.
JanApotheker,

1
Impostazioni layout automatico (prima, fine, superiore, inferiore, NO altezza / larghezza ) + scorrimento disabilitato. e il gioco è fatto. Grazie!
Farhan Arshad,

15

Ho scoperto che non è del tutto insolito in situazioni in cui potresti ancora aver bisogno isScrollEnabled impostato su true per consentire un'interazione UI ragionevole. Un semplice caso per questo è quando si desidera consentire una visualizzazione di testo a espansione automatica ma limitare comunque la sua altezza massima a qualcosa di ragionevole in un UITableView.

Ecco una sottoclasse di UITextView che ho ideato che consente l'espansione automatica con layout automatico, ma che potresti comunque vincolare a un'altezza massima e che gestirà se la vista è scorrevole a seconda dell'altezza. Per impostazione predefinita, la vista si espanderà indefinitamente se hai impostato i tuoi vincoli in quel modo.

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
    }
}

Come bonus c'è il supporto per l'inclusione di testo segnaposto simile a UILabel.


7

Puoi anche farlo senza sottoclasse UITextView. Dai un'occhiata alla mia risposta a Come posso ridimensionare un UITextView al suo contenuto su iOS 7?

Usa il valore di questa espressione:

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

aggiornare il constantdella textViewaltezza 's UILayoutConstraint.


3
Dove lo stai facendo e stai facendo qualcos'altro per assicurarti che UITextView sia impostato? Lo sto facendo e la visualizzazione del testo stessa si dimensiona, ma il testo stesso viene tagliato a metà dell'altezza di UITextView. Il testo è impostato programmaticamente prima di fare le tue cose sopra.
Chadbag,

L'ho trovato più generalmente utile del trucco scrollEnabled = NO, se si desidera impostare manualmente la larghezza.
jchnxu,

3

Una cosa importante da notare:

Poiché UITextView è una sottoclasse di UIScrollView, è soggetta alla proprietà automaticAdjustsScrollViewInsets di UIViewController.

Se stai impostando il layout e TextView è la prima sottoview in una gerarchia di UIViewControllers, il suo contenuto verrà modificato se gli inserimenti automaticiAdjustsScrollViewInsets sono veri a volte causando comportamenti imprevisti nel layout automatico.

Quindi, se riscontri problemi con il layout automatico e le visualizzazioni di testo, prova a impostare automaticallyAdjustsScrollViewInsets = falseil controller di visualizzazione o a spostare TextView in avanti nella gerarchia.


2

Questo più di un commento molto importante

La chiave per capire perché la risposta di vitaminwater funziona sono tre cose:

  1. Sappi che UITextView è una sottoclasse della classe UIScrollView
  2. Scopri come funziona ScrollView e come viene calcolato il suo contentSize. Per di più vedi questo qui risposta e le sue varie soluzioni e commenti.
  3. Comprendi cos'è contentSize e come viene calcolato. Vedi qui e qui . Si potrebbe anche aiutare tale impostazione contentOffsetè probabilmente altro che:

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

Per ulteriori informazioni, consultare objc scrollview e comprendere scrollview


Combinando i tre insieme capiresti facilmente che devi consentire al contenuto intrinseco del textViewSize di funzionare lungo i vincoli AutoLayout di textView per guidare la logica. È quasi come se TextView funzioni come un UILabel

Per farlo, è necessario disabilitare lo scorrimento, il che significa sostanzialmente la dimensione di scrollView, la dimensione di contentSize e in caso di aggiunta di containerView, la dimensione di containerView sarebbe la stessa. Quando sono uguali, NON si ha lo scorrimento. E tu avresti . avere0 contentOffset0 contentOffSet significa che non hai fatto scorrere verso il basso. Nemmeno un punto in basso! Di conseguenza, textView sarà allungato.

Inoltre, non vale nulla che significa che i limiti e il frame di scrollView sono identici. Se scorri verso il basso di 5 punti, ContentOffset sarebbe , mentre sarebbe uguale a0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5


1

Soluzione Plug and Play - Xcode 9

Autolayout proprio come UILabel, con il rilevamento dei collegamenti , la selezione del testo , la modifica e lo scorrimento di UITextView.

Gestisce automaticamente

  • Area sicura
  • Inserzioni di contenuti
  • Imbottitura di frammenti di linea
  • Inserti del contenitore di testo
  • vincoli
  • Viste in pila
  • Stringhe attribuite
  • Qualunque cosa.

Molte di queste risposte mi hanno procurato il 90% lì, ma nessuna era a prova di errore.

Inserisci questa UITextViewsottoclasse e sei a posto.


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

0

La risposta di vitaminwater funziona per me.

Se il testo della visualizzazione di testo rimbalza su e giù durante la modifica, dopo l'impostazione [textView setScrollEnabled:NO];, impostaSize Inspector > Scroll View > Content Insets > Never .

Spero che sia d'aiuto.


0

Posiziona UILabel nascosto sotto la visualizzazione del testo. Righe etichetta = 0. Imposta i vincoli di UITextView in modo che siano uguali a UILabel (centerX, centerY, larghezza, altezza). Funziona anche se si lascia il comportamento di scorrimento di textView.


-1

A proposito, ho creato un UITextView in espansione usando una sottoclasse e sovrascrivendo le dimensioni del contenuto intrinseco. Ho scoperto un bug in UITextView che potresti voler esaminare nella tua implementazione. Ecco il problema:

La visualizzazione del testo espandibile si ridurrebbe per adattarsi al testo crescente se si digitano singole lettere alla volta. Ma se si incolla un mucchio di testo in esso, non si ridurrebbe ma il testo scorrerebbe verso l'alto e il testo in alto non sarà visibile.

La soluzione: sovrascrivi setBounds: nella sottoclasse. Per qualche ragione sconosciuta, l'incollaggio ha causato il valore bounds.origin.y come non-zee (33 in ogni caso che ho visto). Quindi ho scavalcato setBounds: per impostare sempre i limiti. Originale.y a zero. Risolto il problema


-1

Ecco una soluzione rapida:

Questo problema può verificarsi se la proprietà clipsToBounds è stata impostata su false della visualizzazione di testo. Se lo elimini semplicemente, il problema scompare.

myTextView.clipsToBounds = false //delete this line

-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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.