UITextView, который расширяется до текста с помощью автоматического макета

163

У меня есть вид, который выложен полностью с использованием автоматического макета программного обеспечения. У меня есть UITextView в середине представления с элементами выше и ниже. Все работает нормально, но я хочу иметь возможность расширять UITextView по мере добавления текста. Это должно подтолкнуть все ниже под ним, поскольку это расширяется.

Я знаю, как это сделать «пружинами и распорками», но есть ли способ автоматической компоновки? Единственный способ, о котором я могу думать, - это удаление и повторное добавление ограничения каждый раз, когда оно должно расти.

tharris
источник
5
Вы можете сделать IBOutlet с ограничением по высоте для текстового представления и просто изменить константу, когда вы хотите, чтобы она росла, не нужно удалять и добавлять.
rdelmar
4
2017, огромный совет, который часто держит вас: он работает только тогда, когда вы устанавливаете его после VIEWDID> ВНЕШНИЙ ВИД < очень расстраивает, если вы установите текст, скажем, в ViewDidLoad - он не будет работать!
Толстяк
2
Кончик No2: если вы звоните [someView.superView layoutIfNeeded] , то это может работать в viewWillAppear ();)
Яро

Ответы:

30

Представлению, содержащему UITextView, будет назначен его размер с setBoundsпомощью AutoLayout. Итак, это то, что я сделал. В суперпредставлении изначально установлены все остальные ограничения, как и должно быть, и в конце я установил одно специальное ограничение для высоты UITextView и сохранил его в переменной экземпляра.

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

[self addConstraint:_descriptionHeightConstraint];

В setBoundsметоде я затем изменил значение константы.

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

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

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}
ancajic
источник
4
Вы также можете обновить ограничение в 'textViewDidChange:' UITextViewDelegate
LK__
2
Хорошее решение. Обратите внимание, что вы также можете создать ограничение в Интерфейсном Разработчике и подключить его с помощью розетки. В сочетании с textViewDidChangeэтим следует сэкономить немного времени и писать код…
Боб Ворк,
3
Имейте в виду, что textViewDidChange:не будет вызываться при программном добавлении текста в UITextView.
страсть к путешествиям
Предложение @BobVork работает хорошо XCODE 11 / iOS13, установите постоянную ограничения, когда значение текстового представления установлено в viewWillAppear и обновите в textViewDidChange.
ПТКС
549

Сводка: отключите прокрутку вашего текстового представления и не ограничивайте его высоту.

Чтобы сделать это программно, поместите следующий код в 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)
])

Чтобы сделать это в Интерфейсном Разработчике, выберите текстовое представление, снимите отметку с прокруткой, Включенной в Инспекторе Атрибутов, и добавьте ограничения вручную.

Примечание. Если у вас есть другие представления выше или ниже вашего текстового представления, рассмотрите возможность использования UIStackViewих всех.

Vitaminwater
источник
12
У меня работает (iOS 7 @ xcode 5). Предостережение заключается в том, что простое снятие отметки «прокрутка включена» в IB не поможет. Вы должны программно сделать это.
Кеннет Цзян
10
Как установить максимальную высоту в текстовом представлении и затем заставить текстовое представление прокручиваться после того, как его содержание превысило высоту текстового представления?
ma11hew28
3
@iOSGeek просто создайте подкласс UITextView и перезапишите intrinsicContentSizeтак - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Скажи мне, если это работает для тебя. Сам не проверял.
Мануэльвальднер
1
Установите это свойство и поместите UITextView в UIStackView. Voilla - самостоятельно изменяющий размер textView!)
Ник Ков
3
Этот ответ - золото. С Xcode 9 это также работает, если вы отключите флажок в IB.
биография
48

Вот решение для людей, которые предпочитают делать все это с помощью автоматического макета:

По размеру инспектор:

  1. Установите приоритет сопротивления сжатию контента по вертикали на 1000.

  2. Понизьте приоритет высоты ограничения, нажав «Изменить» в Ограничениях. Просто сделайте это меньше 1000.

введите описание изображения здесь

Инспектор атрибутов:

  1. Снимите флажок «Прокрутка включена»
Майкл Ревлис
источник
Это работает отлично. Я не установил высоту. Повышение приоритета вертикального сопротивления сжатия UItextview до 1000 требовалось только. UItextView имеет несколько вставок, поэтому autolayout выдает ошибки, так как UItextview потребуется минимум 32 высоты, даже если в нем нет содержимого, когда прокрутка отключена.
№5
Обновление: ограничение высоты требуется, если вы хотите, чтобы высота была минимум 20 или что-то еще. Другой мудрый 32 будет считаться минимальной высотой.
№5
@Nil Рад, что это помогает.
Майкл Ревлис,
@MichaelRevlis, это прекрасно работает. но когда есть большой текст, то текст не отображается. Вы можете выбрать текст для другой обработки, но он не виден. не могли бы вы помочь мне с этим. ? это происходит, когда я снимаю флажок прокрутки включен. когда я включаю прокрутку просмотра текста, то она отображается идеально, но я хочу, чтобы просмотр текста не прокручивался.
Bhautik Ziniya
@BhautikZiniya, вы пытались создать свое приложение на реальном устройстве? Я думаю, что это может быть симулятор, отображающий ошибку. У меня есть UITextView с размером шрифта текста 100. Он отлично отображается на реальном устройстве, но тексты не видны на симуляторе.
Майкл Ревлис
38

UITextView не предоставляет intrinsicContentSize, поэтому вам нужно создать его подкласс и предоставить его. Чтобы он автоматически увеличивался, сделайте недействительным intrinsicContentSize в layoutSubviews. Если вы используете что-то кроме стандартного contentInset (который я не рекомендую), вам может потребоваться настроить вычисление 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
источник
1
Это хорошо сработало для меня. Кроме того, я бы установил scrollEnabled в NO в Интерфейсном Разработчике для этого Текстового Представления, так как он уже показывает все свое содержимое без необходимости прокрутки. Что еще более важно, если он находится внутри фактического представления прокрутки, вы можете испытать прыжок курсора в нижней части текстового представления, если вы не отключите прокрутку на самом текстовом представлении.
idStar
Это хорошо работает. Интересно, что при переопределении setScrollEnabled:всегда устанавливать его NOвы получите бесконечный цикл для постоянно растущего внутреннего размера контента.
Паскаль
На прошивке 7.0, я также необходимо позвонить [textView sizeToFit]в textViewDidChange:делегате обратного вызова для автоматического макета расти или уменьшить TextView (и пересчитать все зависимые ограничения) после каждого нажатия клавиши.
Followben
6
Не это , кажется , больше не будет необходимости в прошивкой 7.1 Для того, чтобы исправить и обеспечить обратную совместимость, оберните состояние в -(void)layoutSubviewsи обернуть все код в -(CGSize)intrinsicContentSizeс version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
Дэвид Джеймс
1
@DavidJames это все еще необходимо в ios 7.1.2, я не знаю, где вы тестируете.
Софтлион
28

Вы можете сделать это через раскадровку, просто отключив "Scrolling Enabled" :)

StoryBoard

DaNLtR
источник
2
Лучший ответ на этот вопрос
Иван Белко
Это лучшее решение, когда вы работаете с Auto Layout.
JanApotheker
1
Настройки Auto Layout (Leading, Trailing, Top, Bottom, NO Height / Width ) + прокрутка отключена. и там вы идете. Спасибо!
Фархан Аршад
15

Я обнаружил, что в ситуациях, когда вам все еще может понадобиться, isScrollEnabled имеет значение true, чтобы разрешить разумное взаимодействие с пользовательским интерфейсом. Простой случай для этого - когда вы хотите разрешить автоматическое расширение текстового представления, но при этом ограничить его максимальную высоту до чего-то разумного в UITableView.

Вот подкласс UITextView, который я придумал, который позволяет автоматическое расширение с автоматическим макетом, но вы все равно можете ограничиться максимальной высотой и который будет управлять прокруткой вида в зависимости от высоты. По умолчанию представление будет расширяться до бесконечности, если вы настроили ограничения таким образом.

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

В качестве бонуса есть поддержка включения текста-заполнителя, похожего на UILabel.

Майкл Линк
источник
7

Вы также можете сделать это без подклассов UITextView. Взгляните на мой ответ на вопрос «Как изменить размер UITextView для его содержимого на iOS 7?».

Используйте значение этого выражения:

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

обновить constantиз textViewвысоты «S UILayoutConstraint.

ma11hew28
источник
3
Где вы это делаете, и делаете ли вы что-нибудь еще, чтобы убедиться, что UITextView полностью настроен? Я делаю это, и размер самого текстового представления изменяется, но сам текст обрезается на полпути вниз по высоте UITextView. Текст задается программно прямо перед тем, как делать ваши вещи выше.
Чэдбэг
Я нашел это более полезным, чем трюк scrollEnabled = NO, если вы хотите установить ширину вручную.
JNXXU
3

Важно отметить:

Поскольку UITextView является подклассом UIScrollView, он подчиняется автоматически свойству AdjustsScrollViewInsets UIViewController.

Если вы настраиваете макет, а TextView является первым подпредставлением в иерархии UIViewControllers, его contentInsets будут изменены, если автоматически установлено значение trueAdjustsScrollViewInsets, иногда вызывающее непредвиденное поведение в автоматическом макете.

Так что, если у вас есть проблемы с автоматическим макетом и текстовыми представлениями, попробуйте установить automaticallyAdjustsScrollViewInsets = falseна контроллере представления или переместить textView вперед в иерархии.

DaveIngle
источник
2

Это более важный комментарий

Ключ к пониманию того, почему ответ Витаминной воды работает, три вещи:

  1. Знайте, что UITextView является подклассом класса UIScrollView
  2. Понять, как работает ScrollView и как рассчитывается его contentSize. Подробнее см. Здесь ответ и его различные решения и комментарии.
  3. Понять, что такое contentSize и как его рассчитать. Смотрите здесь и здесь . Это также может помочь, что настройки contentOffset, скорее всего, ничего, кроме:

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

Для получения дополнительной информации см. Objc scrollview и понимание scrollview.


Комбинируя все три, вы легко поймете, что вам нужно позволить внутреннему контенту textView sizeView работать в соответствии с ограничениями AutoLayout textView для управления логикой. Это почти так, как будто вы textView работает как UILabel

Чтобы это произошло, вам нужно отключить прокрутку, что в основном означает размер scrollView, размер contentSize и, в случае добавления containerView, тогда размер containerView будет одинаковым. Когда они одинаковы, у вас нет прокрутки. И ты бы сделал . Наличие означает, что вы не прокручивали вниз. Даже 1 балл вниз! В результате textView будет растянут.0 contentOffset0 contentOffSet

Это также ничего не стоит, что означает, что границы и рамка scrollView идентичны. Если вы прокрутите вниз на 5 пунктов, то ваш contentOffset будет , а ваш будет равен0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5

Мед
источник
1

Решение Plug and Play - Xcode 9

Autolayout так же , как UILabel, с обнаружением связи , выделение текста , редактирование и прокруткой из UITextView.

Автоматически обрабатывает

  • Безопасный район
  • Содержание вставок
  • Заполнение фрагмента строки
  • Вставки для текстовых контейнеров
  • Ограничения
  • Стека просмотров
  • Приписанные строки
  • Без разницы.

Многие из этих ответов дали мне 90%, но ни один не был дурака.

Загляните в этот UITextViewподкласс, и все хорошо.


#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
источник
0

Ответ Витаминной воды работает на меня.

Если во время редактирования текст вашего текстового представления подпрыгивает вверх и вниз, после настройки [textView setScrollEnabled:NO];установите Size Inspector > Scroll View > Content Insets > Never.

Надеюсь, поможет.

Барон ч'нг
источник
0

Поместите скрытую UILabel под вашим textview. Линии метки = 0. Установите ограничения UITextView равными UILabel (centerX, centerY, ширина, высота). Работает, даже если вы оставите прокрутку textView.

Юрий
источник
-1

Кстати, я построил расширяющийся UITextView, используя подкласс и переопределяя внутренний размер контента. Я обнаружил ошибку в UITextView, которую вы, возможно, захотите исследовать в своей собственной реализации. Вот проблема:

Раскрывающееся представление текста будет уменьшаться, чтобы вместить растущий текст, если вы набираете по одной букве за раз. Но если вы вставите в него кучу текста, он не будет уменьшаться, но текст будет прокручиваться вверх, а текст сверху не будет виден.

Решение: переопределить setBounds: в вашем подклассе. По какой-то неизвестной причине, из-за вставки значение bounds.origin.y было ненулевым (33 в каждом случае, который я видел). Поэтому я переопределил setBounds: всегда устанавливать bounds.origin.y на ноль. Исправлена ​​проблема.

Алан Маккин
источник
-1

Вот быстрое решение:

Эта проблема может возникнуть, если вы установили для свойства clipsToBounds значение false для вашего текстового представления. Если вы просто удалите его, проблема исчезнет.

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
AG
источник