it-swarm.com.ru

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

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

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

146
tharris

AutoLayout для представления, содержащего UITextView, будет назначен его размер с setBounds. Итак, это то, что я сделал. В суперпредставлении изначально установлены все остальные ограничения, как и должно быть, и в конце я установил одно специальное ограничение для высоты 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];
}
27
ancajic

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

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

518
vitaminwater

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
39
dhallman

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

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

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

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

 enter image description here

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

  1. Снимите флажок «Прокрутка включена»
30
Michael Revlis

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

 StoryBoard

25
DaNLtR

Я обнаружил, что это не является чем-то необычным в ситуациях, когда вам все еще может потребоваться, чтобы для 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.

10
Michael Link

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

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

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

обновить constant высоты textViewUILayoutConstraint.

7
ma11hew28

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

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

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

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

3
DaveIngle

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

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

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

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

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

Если вы прокрутите 5 пунктов вниз, то ваш contentOffset будет 5, а ваш scrollView.bounds.Origin.y - scrollView.frame.Origin.y будет равен 5

1
Honey

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

Autolayout так же, как UILabel, с обнаружением ссылки , выделение текста , редактирование и прокрутка of 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]));
}
0
beebcon

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

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

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

0
Baron Ch'ng

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

0
Yuriy

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

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

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