UITextView使用自动布局扩展为文本

时间:2013-06-01 01:18:25

标签: ios uitextview autolayout nslayoutconstraint

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

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

16 个答案:

答案 0 :(得分:526)

摘要:禁用滚动文本视图,不要限制其高度。

要以编程方式执行此操作,请将以下代码放入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中执行此操作,请选择文本视图,取消选中“属性”检查器中的“滚动已启用”,然后手动添加约束。

注意:如果您在文字视图的上方/下方有其他视图,请考虑使用UIStackView对其进行排列。

答案 1 :(得分:39)

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

答案 2 :(得分:34)

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

在尺寸检查器中:

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

  2. 单击约束中的“编辑”,降低约束高度的优先级。只需使它低于1000。

  3. enter image description here

    在属性检查器中:

    1. 取消选中“滚动已启用”

答案 3 :(得分:27)

AutoLayout将为包含UITextView的视图分配其大小setBounds。所以,这就是我所做的。 superview最初设置了应有的所有其他约束,最后我为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 :(得分:24)

您可以通过故事板进行操作,只需禁用“滚动已启用”:)

This should be the NuGet package you're installing.

答案 5 :(得分:10)

我发现在你可能仍然需要将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的占位符文本。

答案 6 :(得分:7)

您也可以在不继承UITextView的情况下执行此操作。看看my answer to How do I size a UITextView to its content on iOS 7?

使用此表达式的值:

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

更新constant身高textView的{​​{1}}。

答案 7 :(得分:3)

需要注意的重要事项:

由于UITextView是UIScrollView的子类,因此它受UIViewController的automatedAdjustsScrollViewInsets属性的约束。

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

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

答案 8 :(得分:2)

我需要一个文本视图,该视图会自动长大直到达到某个最大高度,然后才能滚动。迈克尔·林克(Michael Link)的答案很好用,但我想看看是否可以提出一些更简单的方法。这是我想出的:

Swift 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()
        }
    }
}


答案 9 :(得分:2)

我看到多个答案建议只需关闭 scrollEnabled。这是最好的解决方案。我写这个答案是为了解释它为什么有效。

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

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

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

不幸的是,此行为未记录在案。 Apple 可以轻松地为我们省去一些麻烦……无需额外的高度限制、子类化等。

答案 10 :(得分:0)

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

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

希望它有所帮助。

答案 11 :(得分:0)

将隐藏的UILabel放在textview下面。标签行= 0.将UITextView的约束设置为等于UILabel(centerX,centerY,width,height)。即使您保留textView的滚动行为也能正常工作。

答案 12 :(得分:0)

即插即用解决方案-Xcode 9

自动布局,就像UILabel一样,具有链接检测文本选择编辑和<{> 1的滚动。

自动处理

  • 安全区
  • 内容插图
  • 行片段填充
  • 文本容器插图
  • 约束
  • 堆栈视图
  • 属性字符串
  • 随便。

很多这样的答案使我达到了90%,但没有一个是万无一失的。

放入此UITextView子类中,您就很好。


UITextView

答案 13 :(得分:-1)

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

如果您一次键入单个字母,展开的文本视图将向下扩展以适应不断增长的文本。但是如果你把一堆文字粘贴到它里面,它就不会长大,但文字会向上滚动,顶部的文字也不在视野之内。

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

答案 14 :(得分:-1)

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

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

myTextView.clipsToBounds = false //delete this line

答案 15 :(得分:-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