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.
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];
}
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.
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
Voici une solution pour ceux qui préfèrent tout faire par mise en page automatique:
En taille inspecteur:
Définissez la priorité de résistance de compression du contenu vertical sur 1000.
Réduisez la priorité de la hauteur de contrainte en cliquant sur "Modifier" dans Contraintes. Il suffit de faire moins de 1000.
Dans l'inspecteur d'attributs:
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.
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
.
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.
Ceci est un commentaire très important
La clé pour comprendre pourquoi la réponse de vitaminwater fonctionne est trois choses:
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 0
contentOffset
. Avoir 0
contentOffSet
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 0
contentOffset
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
Autolayout comme UILabel
, avec le détection de lien , sélection de texte , édition et défilement de UITextView
.
Gère automatiquement
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]));
}
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.
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.
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