web-dev-qa-db-fra.com

UITextView qui s'étend au texte en utilisant la mise en page automatique

J'ai une vue qui est aménagée complètement en utilisant la mise en page automatique par programme. J'ai une UITextView au milieu de la vue avec des éléments au-dessus et au-dessous. Tout fonctionne bien, mais je veux pouvoir développer UITextView à mesure que du texte est ajouté. Cela devrait pousser tout en dessous à mesure qu'il se développe.

Je sais comment procéder de la manière "ressorts et jambes de force", mais existe-t-il un moyen de le faire automatiquement? Le seul moyen auquel je puisse penser est de supprimer et d'ajouter de nouveau la contrainte chaque fois qu'elle doit croître.

146
tharris

La vue contenant UITextView se voit attribuer sa taille avec setBounds par AutoLayout. Donc, c'est ce que j'ai fait. Le superview est initialement configuré avec toutes les autres contraintes comme il se doit, et finalement 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 méthode setBounds, 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];
}
27
ancajic

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

Pour le faire par programme, mettez 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 texte, décochez la case Défilement activé dans l'inspecteur d'attributs et ajoutez les contraintes manuellement.

Remarque: Si vous avez d'autres vues au-dessus/au-dessous de votre vue texte, envisagez d'utiliser une variable UIStackView pour les organiser toutes.

518
vitaminwater

UITextView ne fournit pas un intrinsicContentSize, vous devez donc le sous-classer et en fournir un. Pour le faire croître automatiquement, invalide la propriété intrinsicContentSize dans layoutSubviews. Si vous utilisez autre chose que le contentInset par défaut (ce 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
39
dhallman

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

En taille inspecteur:

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

  2. Réduisez la priorité de la hauteur de contrainte en cliquant sur "Modifier" dans Contraintes. Il suffit de faire moins de 1000.

 enter image description here

Dans l'inspecteur d'attributs:

  1. Décocher "Défilement activé"
30
Michael Revlis

Vous pouvez le faire via le storyboard, il suffit de désactiver "Scrolling Enabled" :)

 StoryBoard

25
DaNLtR

J'ai trouvé que ce n'est pas tout à fait rare dans les situations où vous pouvez toujours avoir besoin de la valeur isScrollEnabled de true pour permettre une interaction d'interface utilisateur raisonnable. Par exemple, vous pouvez autoriser une vue de texte à développement automatique tout en limitant sa hauteur maximale à une valeur raisonnable dans une vue UITableView.

Voici une sous-classe de UITextView que j'ai créée qui permet une expansion automatique avec mise en page automatique, mais que vous pouvez toujours limiter à une hauteur maximale et qui permet de déterminer si la vue peut défiler en fonction de la hauteur. Par défaut, la vue s’agrandira indéfiniment si vos contraintes sont configurées 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 fictif similaire à UILabel.

10
Michael Link

Vous pouvez également le faire sans sous-classe UITextView. Regardez ma réponse à Comment puis-je 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 constant de la textView 's height UILayoutConstraint.

7
ma11hew28

Une chose importante à noter: 

UITextView étant une sous-classe de UIScrollView, il est assujetti à la propriété automaticAdjustsScrollViewInsets de UIViewController. 

Si vous configurez la présentation et que la vue TextView est la première sous-vue de la hiérarchie UIViewControllers, ses contentInsets seront modifiés si la propriétéomatique automatiquement AdjustsScrollViewInsets est vraie, ce qui entraîne parfois un comportement inattendu dans la présentation automatique.

Par conséquent, si vous rencontrez des problèmes de mise en page automatique et de texte, essayez de définir automaticallyAdjustsScrollViewInsets = false sur le contrôleur de vue ou de déplacer le texte dans la hiérarchie.

3
DaveIngle

Ceci est un commentaire très important 

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

  1. Savoir que UITextView est une sous-classe de la classe UIScrollView
  2. Comprenez comment fonctionne ScrollView et comment calcule son contentSize. Pour plus, voir ceci ici answer et ses différentes solutions et commentaires. 
  3. Comprendre ce qu'est contentSize et comment il est calculé. Voir ici et ici

En combinant les trois ensemble, vous comprendrez aisément que vous devez permettre au intrinsic contentSize du textView de fonctionner avec les contraintes de mise en forme automatique du textView pour gérer 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 fondamentalement la taille de scrollView, la taille de contentSize et, en cas d'ajout de containerView, la taille de containerView serait alors la même. Quand ils sont identiques, vous n'avez PAS de défilement. Et vous auriez 0contentOffset. Avoir 0contentOffSet signifie que vous n’avez pas fait défiler la liste. Pas même un point de moins! En conséquence, le textView sera tout étendu. Il est également inutile de savoir que 0contentOffset signifie que les limites et le cadre de scrollView sont identiques. 

Si vous faites défiler 5 points, votre contentOffset serait 5, tandis que votre scrollView.bounds.Origin.y - scrollView.frame.Origin.y serait égal à 5

1
Honey

Solution Plug and Play - Xcode 9

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

Gère automatiquement 

  • Zone protégée
  • Contenu inséré
  • Remplissage de fragment de ligne
  • Inserts de conteneur de texte
  • Contraintes
  • Vues de pile
  • Chaînes Attribuées
  • Peu importe.

90% de ces réponses m'ont amené là-bas à 90%, mais aucune n'était infaillible. 

Abandonnez-vous dans cette sous-classe UITextView et vous serez doué.


#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
beebcon

la réponse de vitaminwater fonctionne pour moi. 

Si le texte de votre affichage de texte rebondit de haut en bas pendant la modification, après avoir défini [textView setScrollEnabled:NO];, définissez Size Inspector > Scroll View > Content Insets > Never.

J'espère que ça aide.

0
Baron Ch'ng

Placez UILabel caché sous votre textview. Lignes de libellé = 0. Définissez les contraintes de UITextView sur égales à UILabel (centerX, centerY, width, height). Fonctionne même si vous laissez le comportement de défilement de textView.

0
Yuriy

Voici une solution rapide:

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

myTextView.clipsToBounds = false //delete this line
0
Eray Alparslan