177

我有一个完全使用自动布局以编程方式布局的视图。我在视图中间有一个 UITextView,上面和下面都有项目。一切正常,但我希望能够在添加文本时扩展 UITextView。随着它的扩展,这应该会将其下方的所有内容向下推。

我知道如何以“弹簧和支柱”的方式做到这一点,但是有没有这样做的自动布局方式?我能想到的唯一方法是在每次需要增长时删除并重新添加约束。

4

17 回答 17

575

摘要:禁用文本视图的滚动,并且不要限制它的高度。

要以编程方式执行此操作,请将以下代码放入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 中执行此操作,请选择文本视图,取消选中 Attributes Inspector 中的 Scrolling Enabled,然后手动添加约束。

注意:如果您在文本视图上方/下方有其他视图,请考虑使用 aUIStackView将它们全部排列。

于 2014-01-22T15:38:17.813 回答
61

对于喜欢通过自动布局完成所有操作的人来说,这是一个解决方案:

在尺寸检查器中:

  1. 将内容压缩阻力优先级垂直设置为 1000。

  2. 通过单击“约束”中的“编辑”来降低约束高度的优先级。让它小于1000就行了。

在此处输入图像描述

在属性检查器中:

  1. 取消选中“启用滚动”
于 2017-07-13T03:22:56.413 回答
38

UITextView 不提供intrinsicContentSize,因此您需要对其进行子类化并提供一个。要使其自动增长,请使 layoutSubviews 中的 intrinsicContentSize 无效。如果您使用默认 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
于 2013-09-25T15:01:15.207 回答
32

setBounds包含 UITextView 的视图将由 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];
}
于 2013-07-18T12:09:56.173 回答
31

你可以通过情节提要做到这一点,只需禁用“启用滚动”:)

故事板

于 2017-04-13T08:16:15.840 回答
19

我发现在您可能仍需要将 isScrollEnabled 设置为 true 以允许进行合理的 UI 交互的情况下,这种情况并不少见。一个简单的情况是,当您希望允许自动扩展文本视图但仍将其最大高度限制为 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 的占位符文本。

于 2017-01-20T18:42:33.073 回答
7

你也可以在没有子类化的情况下做到这一点UITextView。看看我对如何在 iOS 7 上根据其内容调整 UITextView 大小的回答?

使用此表达式的值:

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

更新constanttextView高度UILayoutConstraint

于 2013-10-17T20:24:35.553 回答
5

我看到多个答案建议简单地关闭scrollEnabled。这是最好的解决方案。我正在写这个答案来解释它为什么有效。

UITextView实现intrinsicContentSize属性 if scrollEnabled == NO。getter 方法的反汇编如下所示:

- (CGSize)intrinsicContentSize {
  if (self.scrollEnabled) {
    return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
  } else {
    // Calculate and return intrinsic content size based on current width.
  }
}

这意味着您只需要确保文本视图的宽度受到足够的限制,然后您就可以使用内在的内容高度(通过自动布局内容拥抱/压缩阻力优先级或在手动布局期间直接使用该值)。

不幸的是,没有记录这种行为。苹果本可以很容易地为我们省去一些麻烦……不需要额外的高度限制、子类化等。

于 2021-01-23T17:02:32.803 回答
4

这是一个非常重要的评论

理解为什么维生素水的答案有效的关键是三件事:

  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 的内在 contentSize沿着 textView 的 AutoLayout 约束工作以驱动逻辑。就好像你的 textView 就像 UILabel 一样运作

要做到这一点,您需要禁用滚动,这基本上意味着滚动视图的大小、contentSize 的大小,并且在添加容器视图的情况下,容器视图的大小都将相同。当它们相同时,您将无法滚动。你会有. 拥有意味着你没有向下滚动。连1分都不降!结果,textView 将全部展开。0 contentOffset0 contentOffSet

这也毫无价值,这 意味着滚动视图的边界和框架是相同的。如果您向下滚动 5 个点,那么您的 contentOffset 将是,而您的将等于0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5

于 2018-09-14T18:03:19.173 回答
4

我需要一个文本视图,它会自动增长到某个最大高度,然后变成可滚动的。迈克尔林克的回答很好,但我想看看我是否能想出一些更简单的东西。这是我想出的:

斯威夫特 5.3,Xcode 12

class AutoExpandingTextView: UITextView {

    private var heightConstraint: NSLayoutConstraint!

    var maxHeight: CGFloat = 100 {
        didSet {
            heightConstraint?.constant = maxHeight
        }
    }

    private var observer: NSObjectProtocol?

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        heightConstraint = heightAnchor.constraint(equalToConstant: maxHeight)

        observer = NotificationCenter.default.addObserver(forName: UITextView.textDidChangeNotification, object: nil, queue: .main) { [weak self] _ in
            guard let self = self else { return }
            self.heightConstraint.isActive = self.contentSize.height > self.maxHeight
            self.isScrollEnabled = self.contentSize.height > self.maxHeight
            self.invalidateIntrinsicContentSize()
        }
    }
}


于 2020-09-22T17:13:45.003 回答
3

需要注意的重要一点:

由于 UITextView 是 UIScrollView 的子类,它受制于 UIViewController 的 automaticAdjustsScrollViewInsets 属性。

如果您正在设置布局,并且 TextView 是 UIViewControllers 层次结构中的第一个子视图,那么如果 automaticAdjustsScrollViewInsets 为 true,它将修改其 contentInsets,有时会导致自动布局中出现意外行为。

因此,如果您在自动布局和文本视图方面遇到问题,请尝试automaticallyAdjustsScrollViewInsets = false在视图控制器上进行设置或在层次结构中向前移动 textView。

于 2017-06-10T02:29:10.847 回答
1

即插即用解决方案 - Xcode 9

就像自动布局一样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]));
}
于 2018-08-10T15:37:00.583 回答
0

维生素水的答案对我有用。

如果您的 textview 的文本在编辑过程中上下弹跳,在设置之后[textView setScrollEnabled:NO];,设置Size Inspector > Scroll View > Content Insets > Never.

希望能帮助到你。

于 2018-03-30T15:40:15.750 回答
0

将隐藏的 UILabel 放置在您的文本视图下方。Label lines = 0. 将 UITextView 的约束设置为等于 UILabel (centerX, centerY, width, height)。即使您离开 textView 的滚动行为也可以工作。

于 2018-04-24T14:24:41.873 回答
-1

顺便说一句,我使用子类构建了一个扩展的 UITextView 并覆盖了内在内容大小。我在 UITextView 中发现了一个错误,您可能想在自己的实现中进行调查。这是问题所在:

如果您一次键入单个字母,扩展的文本视图将缩小以适应不断增长的文本。但是,如果您将一堆文本粘贴到其中,它不会向下增长,但文本会向上滚动并且顶部的文本不在视线范围内。

解决方案:在您的子类中覆盖 setBounds:。由于某些未知原因,粘贴导致 bounds.origin.y 值不是 zee (在我看到的每种情况下都是 33)。所以我覆盖了 setBounds: 总是将 bounds.origin.y 设置为零。修复了问题。

于 2014-09-05T00:03:42.927 回答
-1

这是一个快速的解决方案:

如果您将 textview 的 clipsToBounds 属性设置为 false,则可能会出现此问题。如果您只是删除它,问题就会消失。

myTextView.clipsToBounds = false //delete this line
于 2018-08-16T13:46:03.467 回答
-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
于 2016-01-22T05:27:23.317 回答