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

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

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

Ответ 1

Представлению, содержащему UITextView, будет присвоен его размер с помощью setBounds by 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];
}

Ответ 2

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

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

Чтобы сделать это в Interface Builder, выберите текстовое представление, снимите флажок Scrolling Enabled в инспекторе атрибутов и добавьте ограничения вручную.

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

Ответ 3

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

Ответ 4

Здесь решение для людей, которые предпочитают делать все путем автоматической компоновки:

In Size Inspector:

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

  2. Опустите приоритет высоты ограничения, щелкнув "Изменить" в "Ограничения". Просто сделайте это меньше 1000.

enter image description here

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

  1. Снимите флажок "Прокрутка включена"

Ответ 5

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

StoryBoard

Ответ 6

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

Ответ 8

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

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

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

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

Ответ 9

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

Ключ к пониманию того, почему витаминная вода работает, - это три вещи:

  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 textView работать в соответствии с ограничениями AutoLayout textView для управления логикой. Это почти как если бы вы textView функционировали как UILabel

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

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

Ответ 10

Ответ на витаминную воду работает для меня.

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

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

Ответ 11

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

Ответ 12

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

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

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

  • Безопасный район
  • Вставки контента
  • Отступ фрагмента строки
  • Вставки текстовых контейнеров
  • Ограничения
  • Просмотры стека
  • Атрибутивные строки
  • Без разницы.

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

UITextView в этот подкласс 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]));
}

Ответ 13

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

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

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

Ответ 14

Здесь быстрое решение:

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

myTextView.clipsToBounds = false //delete this line

Ответ 15

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