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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16868117/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:45:34  来源:igfitidea点击:

UITextView that expands to text using auto layout

iosuitextviewautolayoutnslayoutconstraint

提问by tharris

I have a view that is laid out completely using auto layout programmatically. I have a UITextView in the middle of the view with items above and below it. Everything works fine, but I want to be able to expand UITextView as text is added. This should push everything below it down as it expands.

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

I know how to do this the "springs and struts" way, but is there an auto layout way of doing this? The only way I can think of is by removing and re-adding the constraint every time it needs to grow.

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

采纳答案by ancajic

The view containing UITextView will be assigned its size with setBoundsby AutoLayout. So, this is what I did. The superview is initially set up all the other constraints as they should be, and in the end I put one special constraint for UITextView's height, and I saved it in an instance variable.

包含 UITextView 的视图将setBounds通过 AutoLayout分配其大小。所以,这就是我所做的。超级视图最初设置了所有其他约束,最后我为 UITextView 的高度设置了一个特殊约束,并将其保存在一个实例变量中。

_descriptionHeightConstraint = [NSLayoutConstraint constraintWithItem:_descriptionTextView
                                 attribute:NSLayoutAttributeHeight 
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:nil 
                                 attribute:NSLayoutAttributeNotAnAttribute 
                                multiplier:0.f 
                                 constant:100];

[self addConstraint:_descriptionHeightConstraint];

In the setBoundsmethod, I then changed the value of the constant.

setBounds方法中,我然后更改了常量的值。

-(void) setBounds:(CGRect)bounds
{
    [super setBounds:bounds];

    _descriptionTextView.frame = bounds;
    CGSize descriptionSize = _descriptionTextView.contentSize;

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}

回答by vitaminwater

Summary: Disable scrolling of your text view, and don't constraint its height.

总结:禁用文本视图的滚动,并且不限制其高度。

To do this programmatically, put the following code in viewDidLoad:

要以编程方式执行此操作,请将以下代码放入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)
])

To do this in Interface Builder, select the text view, uncheck Scrolling Enabled in the Attributes Inspector, and add the constraints manually.

要在 Interface Builder 中执行此操作,请选择文本视图,在 Attributes Inspector 中取消选中 Scrolling Enabled,然后手动添加约束。

Note: If you have other view/s above/below your text view, consider using a UIStackViewto arrange them all.

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

回答by Michael Revlis

Here's a solution for people who prefer to do it all by auto layout:

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

In Size Inspector:

在尺寸检查器中:

  1. Set content compression resistance priority vertical to 1000.

  2. Lower the priority of constraint height by click "Edit" in Constraints. Just make it less than 1000.

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

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

enter image description here

在此处输入图片说明

In Attributes Inspector:

在属性检查器中:

  1. Uncheck "Scrolling Enabled"
  1. 取消选中“启用滚动”

回答by dhallman

UITextView doesn't provide an intrinsicContentSize, so you need to subclass it and provide one. To make it grow automatically, invalidate the intrinsicContentSize in layoutSubviews. If you use anything other than the default contentInset (which I do not recommend), you may need to adjust the intrinsicContentSize calculation.

UITextView 不提供内在内容大小,因此您需要对其进行子类化并提供一个。要使其自动增长,请使 layoutSubviews 中的内在内容大小无效。如果您使用默认 contentInset 以外的任何内容(我不推荐),您可能需要调整 internalContentSize 计算。

@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

回答by DaNLtR

You can do it through storyboard, just disable "Scrolling Enabled":)

您可以通过故事板来完成,只需禁用“启用滚动”:)

StoryBoard

故事板

回答by Michael Link

I've found it's not entirely uncommon in situations where you may still need isScrollEnabled set to true to allow a reasonable UI interaction. A simple case for this is when you want to allow an auto expanding text view but still limit it's maximum height to something reasonable in a UITableView.

我发现在您可能仍然需要将 isScrollEnabled 设置为 true 以允许合理的 UI 交互的情况下,这并不少见。一个简单的情况是,当您希望允许自动扩展文本视图但仍将其最大高度限制为 UITableView 中的合理值时。

Here's a subclass of UITextView I've come up with that allows auto expansion with auto layout but that you could still constrain to a maximum height and which will manage whether the view is scrollable depending on the height. By default the view will expand indefinitely if you have your constraints setup that way.

这是我提出的 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
    }
}

As a bonus there's support for including placeholder text similar to UILabel.

作为奖励,支持包含类似于 UILabel 的占位符文本。

回答by ma11hew28

You can also do it without subclassing UITextView. Have a look at my answer to How do I size a UITextView to its content on iOS 7?

您也可以在没有子类化的情况下执行此操作UITextView。看看我在 iOS 7 上如何将 UITextView 的大小调整到其内容的答案?

Use the value of this expression:

使用这个表达式的值:

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

to update the constantof the textView's height UILayoutConstraint.

更新constant了的textView的高度UILayoutConstraint

回答by DaveIngle

An important thing to note:

需要注意的重要事项:

Since UITextView is a subclass of UIScrollView, it is subject to the automaticallyAdjustsScrollViewInsets property of UIViewController.

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

If you are setting up the layout and the TextView is the the first subview in a UIViewControllers hierarchy, it will have its contentInsets modified if automaticallyAdjustsScrollViewInsets is true sometimes causing unexpected behaviour in auto layout.

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

So if you're having problems with auto layout and text views, try setting automaticallyAdjustsScrollViewInsets = falseon the view controller or moving the textView forward in the hierarchy.

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

回答by Honey

This more of a very important comment

这更重要的评论

Key to understanding why vitaminwater's answerworks are three things:

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

  1. Know that UITextView is a subclass of UIScrollView class
  2. Understand how ScrollView works and how its contentSize is calculated. For more see this hereanswer and its various solutions and comments.
  3. Understand what contentSize is and how its calculated. See hereand here. It might also help that setting contentOffsetis likelynothing but:
  1. 知道 UITextView 是 UIScrollView 类的子类
  2. 了解 ScrollView 的工作原理以及它的 contentSize 是如何计算的。有关更多信息,请参阅 此处的答案及其各种解决方案和评论。
  3. 了解 contentSize 是什么以及它是如何计算的。请参阅此处此处。这也可以帮助该设置contentOffset有可能只是:

func setContentOffset(offset: CGPoint)
{
    CGRect bounds = self.bounds
    bounds.origin = offset
    self.bounds = bounds
}

For more see objc scrollviewand understanding scrollview

有关更多信息,请参阅objc scrollview了解 scrollview



Combining the three together you'd easily understand that you need allow the the textView's intrinsic contentSizeto work along AutoLayout constraints of the textView to drive the logic. It's almost as if you're textView is functioning like a UILabel

将这三者结合在一起,您很容易理解您需要允许textView的内在 contentSize与 textView 的 AutoLayout 约束一起工作以驱动逻辑。就好像你的 textView 的功能就像 UILabel

To make that happen you need to disable scrolling which basically means the scrollView's size, the contentSize's size and in case of adding a containerView, then the containerView's size would all be the same. When they're the same you have NO scrolling. And you'd have 0contentOffset. Having 0contentOffSetmeans you've not scrolled down. Not even a 1 point down! As a result the textView will be all stretched out.

要实现这一点,您需要禁用滚动,这基本上意味着 scrollView 的大小、contentSize 的大小,并且在添加 containerView 的情况下,containerView 的大小将全部相同。当它们相同时,您没有滚动。你会有. 有意味着你没有向下滚动。连1分都没有!结果 textView 将全部被拉伸。0contentOffset0contentOffSet

It's also worth nothing that 0contentOffsetmeans that the scrollView's bounds and frame are identical. If you scroll down 5 points then your contentOffset would be 5, while your scrollView.bounds.origin.y - scrollView.frame.origin.ywould be equal to 5

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

回答by beebcon

Plug and Play Solution - Xcode 9

即插即用解决方案 - Xcode 9

Autolayoutjust like UILabel, with the link detection, text selection, editingand scrollingof UITextView.

Autolayout就像UILabel,带有链接检测文本选择编辑滚动UITextView

Automatically handles

自动处理

  • Safe area
  • Content insets
  • Line fragment padding
  • Text container insets
  • Constraints
  • Stack views
  • Attributed strings
  • Whatever.
  • 安全区
  • 内容插入
  • 行片段填充
  • 文本容器插入
  • 约束
  • 堆栈视图
  • 属性字符串
  • 任何。

A lot of these answers got me 90% there, but none were fool-proof.

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

Drop in this UITextViewsubclass and you're good.

加入这个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]));
}