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:
-
Установите приоритет сопротивления сжатию содержимого по вертикали до 1000.
-
Опустите приоритет высоты ограничения, щелкнув "Изменить" в "Ограничения". Просто сделайте это меньше 1000.
![enter image description here]()
Инспектор атрибутов:
- Снимите флажок "Прокрутка включена"
Ответ 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.
Ответ 7
Вы также можете сделать это без подкласса UITextView
. Посмотрите на мой ответ: Как мне настроить UITextView на его содержимое на iOS 7?
Используйте значение этого выражения:
[textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height
чтобы обновить constant
высоты textView
height UILayoutConstraint
.
Ответ 8
Важно отметить:
Так как UITextView является подклассом UIScrollView, он подчиняется свойству autoAdjustsScrollViewInsets UIViewController.
Если вы настраиваете макет, а TextView является первым подвью в иерархии UIViewControllers, он будет иметь свои вставки содержимого, если автоматическиAdjustsScrollViewInsets является истинным, иногда вызывает неожиданное поведение в автоматическом макете.
Итак, если у вас возникли проблемы с автоматическим макетом и текстовыми представлениями, попробуйте установить automaticallyAdjustsScrollViewInsets = false
на контроллере представления или перемещая textView вперед в иерархии.
Ответ 9
Это более важный комментарий
Ключ к пониманию того, почему витаминная вода работает, - это три вещи:
- Знайте, что UITextView является подклассом класса UIScrollView
- Понять, как работает ScrollView и как рассчитывается его contentSize. Подробнее см. Здесь ответ и его различные решения и комментарии.
- Понять, что такое 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