UITextView qui se développe en texte à l'aide de la mise en page automatique

163

J'ai une vue qui est entièrement présentée en utilisant la mise en page automatique par programmation. J'ai un UITextView au milieu de la vue avec des éléments au-dessus et en dessous. Tout fonctionne bien, mais je veux pouvoir étendre UITextView au fur et à mesure que du texte est ajouté. Cela devrait pousser tout ce qui se trouve en dessous à mesure qu'il se développe.

Je sais comment faire cela de la manière "ressorts et entretoises", mais y a-t-il un moyen de mise en page automatique? La seule façon dont je peux penser est de supprimer et de rajouter la contrainte à chaque fois qu'elle doit se développer.

tharris
la source
5
Vous pouvez créer un IBOutlet avec une contrainte de hauteur pour la vue de texte, et simplement modifier la constante lorsque vous voulez qu'elle s'agrandisse, pas besoin de supprimer et d'ajouter.
rdelmar
4
2017, un énorme conseil qui vous retient souvent: IL NE FONCTIONNE QUE SI VOUS LE RÉGLEZ APRÈS VIEWDID> APPEAR < c'est très frustrant si vous définissez le texte à ViewDidLoad - cela ne fonctionnera pas!
Fattie
2
Astuce No2: si vous appelez [someView.superView layoutIfNeeded] ALORS cela pourrait fonctionner dans viewWillAppear ();)
Yaro

Réponses:

30

La vue contenant UITextView se verra attribuer sa taille setBoundspar AutoLayout. Alors, c'est ce que j'ai fait. Le superview est initialement configuré toutes les autres contraintes comme elles devraient l'être, et à la fin j'ai mis une contrainte spéciale pour la hauteur de UITextView, et je l'ai sauvegardée dans une variable d'instance.

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

[self addConstraint:_descriptionHeightConstraint];

Dans la setBoundsméthode, j'ai ensuite changé la valeur de la constante.

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

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

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}
ancajic
la source
4
Vous pouvez également mettre à jour la contrainte dans 'textViewDidChange:' de l'UITextViewDelegate
LK__
2
Belle solution. Notez que vous pouvez également créer la contrainte dans Interface Builder et la connecter à l'aide d'une prise. Combiné à l'utilisation de textViewDidChangecela, cela devrait éviter des tracas de sous-classification et d'écriture de code…
Bob Vork
3
Gardez à l'esprit que textViewDidChange:cela ne sera pas appelé lors de l'ajout de texte à UITextView par programme.
wanderlust
La suggestion @BobVork fonctionne bien XCODE 11 / iOS13, définissez une constante de contrainte lorsque la valeur de vue du texte est définie dans viewWillAppear et mise à jour dans textViewDidChange.
ptc
549

Résumé: désactivez le défilement de votre vue texte et ne contraignez pas sa hauteur.

Pour ce faire par programme, placez le code suivant dans 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)
])

Pour ce faire dans Interface Builder, sélectionnez la vue de texte, décochez Défilement activé dans l'inspecteur d'attributs et ajoutez les contraintes manuellement.

Remarque: Si vous avez d'autres vues au-dessus / en dessous de votre vue texte, pensez à utiliser a UIStackViewpour toutes les organiser.

eau vitaminée
la source
12
Fonctionne pour moi (iOS 7 @ xcode 5). La mise en garde est que simplement décocher «défilement activé» dans IB n'aidera pas. Vous devez le faire par programme.
Kenneth Jiang
10
Comment définir une hauteur maximale sur la vue texte, puis faire défiler la vue texte une fois que son contenu a dépassé la hauteur de la vue texte?
ma11hew28
3
@iOSGeek crée simplement une sous-classe UITextView et écrase intrinsicContentSizecomme ceci - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Dites-moi si ça marche pour vous. Je ne l'ai pas testé moi-même.
manuelwaldner
1
Définissez cette propriété et placez UITextView dans UIStackView. Voilla - textView à redimensionnement automatique!)
Nik Kov
3
Cette réponse est en or. Avec Xcode 9, cela fonctionne également si vous désactivez la case à cocher dans IB.
bio
48

Voici une solution pour les personnes qui préfèrent tout faire par mise en page automatique:

Dans l'inspecteur de taille:

  1. Définissez la priorité verticale de la résistance à la compression du contenu sur 1000.

  2. Réduisez la priorité de la hauteur de contrainte en cliquant sur "Modifier" dans Contraintes. Faites simplement moins de 1000.

entrez la description de l'image ici

Dans l'inspecteur d'attributs:

  1. Décochez "Défilement activé"
Michael Revlis
la source
Cela fonctionne parfaitement. Je n'ai pas réglé la hauteur. L'augmentation de la priorité de la résistance à la compression verticale d'UItextview à 1000 était seulement nécessaire. UItextView a quelques encarts, c'est pourquoi la mise en page automatique donne des erreurs car UItextview aura besoin d'au moins 32 hauteurs, même si aucun contenu ne s'y trouve, lorsque le défilement est désactivé.
nr5 le
Mise à jour: une contrainte de hauteur est requise si vous souhaitez que la hauteur soit minimale à 20 ou autre. Sinon, 32 sera considéré comme la hauteur minimale.
nr5 le
@Nil Heureux d'entendre que cela aide.
Michael Revlis
@MichaelRevlis, cela fonctionne très bien. mais lorsqu'il y a un gros texte, le texte ne s'affiche pas. vous pouvez sélectionner du texte pour effectuer un autre traitement, mais il n'est pas visible. pouvez-vous s'il vous plaît m'aider avec ça. ? cela se produit lorsque je décoche l'option de défilement activé. lorsque je active le défilement de la vue texte, il s'affiche parfaitement, mais je veux que la vue texte ne défile pas.
Bhautik Ziniya
@BhautikZiniya, avez-vous essayé de créer votre application sur un vrai appareil? Je pense que ce pourrait être un simulateur affichant un bug. J'ai un UITextView avec une taille de police de texte 100. Il s'affiche bien sur un appareil réel mais les textes sont invisibles sur un simulateur.
Michael Revlis
38

UITextView ne fournit pas de intrinsicContentSize, vous devez donc le sous-classer et en fournir un. Pour le faire grandir automatiquement, invalidez intrinsicContentSize dans layoutSubviews. Si vous utilisez autre chose que le contentInset par défaut (que je ne recommande pas), vous devrez peut-être ajuster le calcul 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
la source
1
Cela a bien fonctionné pour moi. De plus, je définirais 'scrollEnabled' sur NO dans Interface Builder pour cette vue de texte, car il affiche déjà tout son contenu sans faire défiler nécessaire. Plus important encore, s'il se trouve dans une vue de défilement réelle, vous pouvez voir le curseur sauter près du bas de la vue de texte, si vous ne désactivez pas le défilement sur la vue de texte elle-même.
idStar
Cela fonctionne bien. Il est intéressant de noter que lorsque vous remplacez setScrollEnabled:pour toujours le définir, NOvous obtiendrez une boucle infinie pour une taille de contenu intrinsèque toujours croissante.
Pascal
Sur iOS 7.0, j'avais également besoin d'appeler [textView sizeToFit]le textViewDidChange:rappel de délégué pour la mise en page automatique pour agrandir ou réduire la vue de texte (et recalculer toutes les contraintes dépendantes) après chaque frappe.
suivez le
6
Cela ne semble plus nécessaire dans iOS 7.1.Pour corriger et fournir une compatibilité descendante, enveloppez la condition -(void)layoutSubviewset enveloppez tout le code -(CGSize)intrinsicContentSizeavec version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
David James
1
@DavidJames cela est toujours nécessaire dans ios 7.1.2, je ne sais pas où vous testez.
Softlion
28

Vous pouvez le faire via le storyboard, désactivez simplement "Défilement activé" :)

StoryBoard

DaNLtR
la source
2
La meilleure réponse à cette question
Ivan Byelko
C'est la meilleure solution lorsque vous travaillez avec la mise en page automatique.
JanApotheker
1
Paramètres de mise en page automatique (interligne, fin, haut, bas, pas de hauteur / largeur ) + défilement désactivé. et là vous allez. Merci!
Farhan Arshad
15

J'ai trouvé que ce n'est pas tout à fait rare dans les situations où vous pouvez toujours avoir besoin de isScrollEnabled défini sur true pour permettre une interaction raisonnable avec l'interface utilisateur. Un cas simple pour cela est lorsque vous souhaitez autoriser une vue de texte à expansion automatique tout en limitant sa hauteur maximale à quelque chose de raisonnable dans un UITableView.

Voici une sous-classe de UITextView que j'ai proposée qui permet l'expansion automatique avec mise en page automatique mais que vous pouvez toujours contraindre à une hauteur maximale et qui gérera si la vue peut faire défiler en fonction de la hauteur. Par défaut, la vue s'agrandit indéfiniment si vous avez configuré vos contraintes de cette façon.

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

En prime, il est possible d'inclure un texte d'espace réservé similaire à UILabel.

Michael Link
la source
7

Vous pouvez également le faire sans sous-classer UITextView. Jettes un coup d'oeil à ma réponse à Comment dimensionner un UITextView en fonction de son contenu sur iOS 7?

Utilisez la valeur de cette expression:

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

mettre à jour la constantde la textViewhauteur de » UILayoutConstraint.

ma11hew28
la source
3
Où faites-vous cela et faites-vous autre chose pour vous assurer que l'UITextView est bien configuré? Je fais cela, et la vue de texte elle-même taille, mais le texte lui-même est coupé à mi-chemin de la hauteur de UITextView. Le texte est défini par programme juste avant d'effectuer vos tâches ci-dessus.
chadbag
J'ai trouvé cela plus généralement utile que l'astuce scrollEnabled = NO, si vous souhaitez définir la largeur manuellement.
jchnxu
3

Une chose importante à noter:

Étant donné que UITextView est une sous-classe de UIScrollView, il est soumis à la propriété automaticAdjustsScrollViewInsets de UIViewController.

Si vous configurez la mise en page et que TextView est la première sous-vue d'une hiérarchie UIViewControllers, son contentInsets sera modifié si automaticAdjustsScrollViewInsets est vrai, provoquant parfois un comportement inattendu dans la mise en page automatique.

Donc, si vous rencontrez des problèmes avec la disposition automatique et les vues de texte, essayez de définir automaticallyAdjustsScrollViewInsets = falsesur le contrôleur de vue ou de déplacer le textView vers l'avant dans la hiérarchie.

DaveIngle
la source
2

C'est plus un commentaire très important

La clé pour comprendre pourquoi la réponse de vitaminwater fonctionne sont trois choses:

  1. Sachez que UITextView est une sous-classe de la classe UIScrollView
  2. Comprenez comment fonctionne ScrollView et comment son contentSize est calculé. Pour en savoir plus, voyez ceci ici réponse et ses différentes solutions et commentaires.
  3. Comprenez ce qu'est contentSize et comment il est calculé. Voir ici et ici . Cela pourrait également aider que le paramètre ne contentOffsetsoit probablement rien d'autre que:

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

Pour en savoir plus, voir objc scrollview et comprendre scrollview


En combinant les trois ensemble, vous comprendrez facilement que vous devez permettre au contentSize intrinsèque du textView de fonctionner avec les contraintes de mise en page automatique du textView pour piloter la logique. C'est presque comme si votre textView fonctionnait comme un UILabel

Pour ce faire, vous devez désactiver le défilement, ce qui signifie essentiellement la taille de scrollView, la taille de contentSize et en cas d'ajout d'un containerView, la taille de containerView serait la même. Quand ils sont identiques, vous n'avez PAS de défilement. Et vous auriez . Ayant0 contentOffset0 contentOffSet signifie que vous n'avez pas fait défiler vers le bas. Pas même un point de moins! En conséquence, le textView sera tout étiré.

Cela ne vaut également rien qui signifie que les limites et le cadre de scrollView sont identiques. Si vous faites défiler vers le bas de 5 points, votre contentOffset serait alors que votre serait égal à0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5

Mon chéri
la source
1

Solution prête à l'emploi - Xcode 9

Autolayout comme UILabel, avec la détection de lien , la sélection de texte , l' édition et le défilement de UITextView.

Gère automatiquement

  • Zone protégée
  • Encarts de contenu
  • Remplissage de fragment de ligne
  • Encarts de conteneur de texte
  • Contraintes
  • Vues de la pile
  • Chaînes attribuées
  • Peu importe.

Un grand nombre de ces réponses m'ont permis d'atteindre 90%, mais aucune n'était infaillible.

Descendez dans cette UITextViewsous - classe et vous êtes bon.


#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
la source
0

La réponse de vitaminwater fonctionne pour moi.

Si le texte de votre vue de texte rebondit de haut en bas pendant la modification, après le réglage [textView setScrollEnabled:NO];, définissezSize Inspector > Scroll View > Content Insets > Never .

J'espère que ça aide.

Baron Ch'ng
la source
0

Placez UILabel caché sous votre vue de texte. Lignes d'étiquette = 0. Définissez les contraintes de UITextView pour qu'elles soient égales à UILabel (centerX, centerY, largeur, hauteur). Fonctionne même si vous laissez le comportement de défilement de textView.

Yuriy
la source
-1

BTW, j'ai construit un UITextView en expansion en utilisant une sous-classe et en remplaçant la taille du contenu intrinsèque. J'ai découvert un bogue dans UITextView que vous voudrez peut-être étudier dans votre propre implémentation. Voici le problème:

La vue de texte extensible s'agrandit pour s'adapter au texte croissant si vous tapez des lettres uniques à la fois. Mais si vous collez un tas de texte dedans, il ne grossirait pas mais le texte défilerait vers le haut et le texte en haut était hors de vue.

La solution: Remplacez setBounds: dans votre sous-classe. Pour une raison inconnue, le collage a rendu la valeur bounds.origin.y non-zee (33 dans chaque cas que j'ai vu). J'ai donc surchargé setBounds: pour toujours mettre le bounds.origin.y à zéro. Correction du problème.

Alan McKean
la source
-1

Voici une solution rapide:

Ce problème peut se produire si vous avez défini la propriété clipsToBounds sur false de votre textview. Si vous le supprimez simplement, le problème disparaît.

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