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


163

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

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


5
Ви можете зробити IBOutlet обмеженням висоти для перегляду тексту та просто змінити константу, коли ви хочете, щоб вона зростала, не потрібно видаляти та додавати.
rdelmar

4
2017 рік, величезна порада, яка часто вас стримує: ТОЛЬКО РОБОТИ, АКО ВИ ВИСТАВЛИХ ЇЇ ПІСЛЯ VIEWDID> APPEAR < це дуже засмучує, якщо ви встановите текст скажу на ViewDidLoad - він не працюватиме!
Fattie

2
порада №2: якщо ви зателефонуєте [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];
}

4
Ви також можете оновити обмеження в "textViewDidChange:" UITextViewDelegate
LK__

2
Приємне рішення. Зауважте, що ви також можете створити обмеження в Interface Builder і з'єднати його за допомогою розетки. У поєднанні з textViewDidChangeцим використанням потрібно врятувати деякі клопоти підкласифікації та написання коду…
Bob Vork

3
Майте на увазі, що textViewDidChange:при додаванні тексту до програм UITextView програма не буде викликана.
блукання

Пропозиція @BobVork добре працює XCODE 11 / iOS13, встановити постійну обмеження, коли значення перегляду тексту встановлено у viewWillAppear та оновить у textViewDidChange.
ptc

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)
])

Для цього у програмі Interface Builder виберіть текстовий вигляд, зніміть прапорець Прокручування увімкнено в інспекторі атрибутів та додайте обмеження вручну.

Примітка. Якщо у вас є інші перегляди над / під текстом, спробуйте скористатись a, UIStackViewщоб упорядкувати їх усі.


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; }. Скажіть, чи працює це для вас. Не перевіряв це сам.
manuelwaldner

1
Встановіть цю властивість і поставте UITextView в UIStackView. Voilla - самостійний розмір textView!)
Нік Ков

3
Ця відповідь - золота. З Xcode 9 він також працює, якщо вимкнути прапорець у IB.
біо

48

Ось рішення для людей, які вважають за краще це робити за допомогою автоматичного макета:

Інспектор розміру:

  1. Встановіть вертикальний пріоритет стиснення вмісту на 1000.

  2. Зменшіть пріоритет висоти обмеження, натиснувши "Змінити" в "Обмеження". Просто зробіть його менше 1000.

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

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

  1. Зніміть прапорець "Увімкнено прокрутку"

Це працює чудово. Я не встановлював висоту. Підвищення пріоритетності вертикального опору стиснення UItextview до 1000 було потрібно лише. UItextView має деякі вставки, тому для автоматичного вимикання виникають помилки, оскільки для UItextview потрібно буде не менше 32 висоти, навіть якщо в ньому немає вмісту, коли прокрутка вимкнена.
nr5

Оновлення: обмеження висоти потрібно, якщо ви хочете, щоб висота була мінімальною до 20 або що-небудь інше. Інші мудрі 32 вважатимуться як мінімальна висота.
nr5

@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

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, я не знаю, де ви тестуєте.
Softlion

28

Ви можете зробити це через раскадровку, просто вимкніть "Увімкнено прокрутку" :)

StoryBoard


2
Найкраща відповідь на це запитання
Іван Бєлько

Це найкраще рішення під час роботи з автоматичною компонуванням.
JanApotheker

1
Налаштування автоматичного макета (провідна, зворотна, верх, знизу, висота / ширина NO ) + прокрутка вимкнена. і там ти йдеш. Дякую!
Фархан Аршад

15

Я виявив, що це не зовсім рідко в ситуаціях, коли вам все-таки знадобиться функція iscrollEnabled, встановлена ​​в 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.


3
Де ви це робите і чи робите щось інше, щоб переконатися, що UITextView все налаштовано? Я роблю це, і сам перегляд тексту розміром, але сам текст відрізається наполовину вниз по висоті UITextView. Текст задається програмно право перед тим, як робити речі вище.
chadbag

Я вважаю це більш корисним, ніж трюк scrollEnabled = NO, якщо ви хочете встановити ширину вручну.
jchnxu

3

Важливо зазначити:

Оскільки UITextView є підкласом UIScrollView, він підпадає під властивість автоматичноAdjustsScrollViewInsets UIViewController.

Якщо ви налаштовуєте макет, а TextView - це перший підвід в ієрархії UIViewControllers, його зміна буде змінено, якщо автоматичноAdjustsScrollViewInsets істинно іноді викликає несподіване поведінку в автоматичному макеті.

Тож якщо у вас виникають проблеми з автоматичним компонуванням та переглядом тексту, спробуйте встановити automaticallyAdjustsScrollViewInsets = falseна контролері подання або перемістити textView в ієрархії.


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 та розуміння прокрутки


Комбінуючи три разом , ви б легко зрозуміти , що вам потрібно дозволити в TextView по внутрішньої contentSize до роботи по AutoLayout обмежень TextView для управління логікою. Це майже так, ніби текстView функціонує як UILabel

Щоб цього сталося, вам потрібно відключити прокрутку, що в основному означає розмір scrollView, розмір contentSize та у випадку додавання контейнераView, розмір контейнераView буде таким самим. Коли вони однакові, у вас НІМАЄ прокручування. І ти мав би . Це означає, що ви не прокрутили вниз. Навіть 1 бал вниз! В результаті textView буде розтягнутий.0 contentOffset0 contentOffSet

Також нічого не варто, що означає, що межі та рамка scrollView однакові. Якщо ви прокрутите вниз 5 балів, то ваш contentOffset був би , тоді як ваш буде рівний0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5


1

Plug and Play Solution - 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]));
}

0

Відповідь вітамінної води працює на мене.

Якщо під час редагування текст вашого перегляду тексту підстрибує вгору та вниз, встановіть його [textView setScrollEnabled:NO];, встановіть Size Inspector > Scroll View > Content Insets > Never.

Сподіваюся, це допомагає.


0

Помістіть прихований UILabel під текстовим переглядом. Рядки міток = 0. Встановіть обмеження UITextView рівними UILabel (centerX, центр Y, ширина, висота). Працює, навіть якщо ви залишаєте поведінку прокрутки textView.


-1

До речі, я створив UITextView, що розширюється, використовуючи підклас та переважаючий внутрішній розмір вмісту. Я виявив помилку в UITextView, яку ви, можливо, захочете дослідити у власній реалізації. Ось проблема:

Перегляд тексту, що розширюється, перетворюватиметься на вміст тексту, що зростає, якщо ви вводите одиничні букви одночасно. Але якщо вставити в неї купу тексту, вона не зростатиметься вниз, а текст прокручується вгору, а текст у верхній частині випадає з поля зору.

Рішення: Замініть setBounds: у своєму підкласі. З якоїсь незрозумілої причини вставка призвела до того, що значення bounds.origin.y не має значення (33 у кожному випадку, який я бачив). Тож я переосмислив setBounds: щоб завжди встановити знаки bounds.origin.y на нуль. Виправлена ​​проблема.


-1

Ось швидке рішення:

Ця проблема може виникнути, якщо ви встановили властивість clipsToBounds неправдивою для перегляду тексту. Якщо просто видалити його, проблема зникає.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.