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


163

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

我知道如何以“弹簧和支柱”的方式进行操作,但是是否有自动布局的方式来执行此操作?我能想到的唯一方法是每次需要增长时都删除并重新添加约束。


5
您可以将IBOutlet设置为文本视图的高度约束,并且只需在要增长常量时对其进行修改,而无需删除和添加。
rdelmar

4
2017年,一个经常提示您的重要提示: 当您在VIEWDID> APPEAR <之后设置它,它才会起作用,如果您在ViewDidLoad中设置文字,那将非常令人沮丧-它将无法工作!
Fattie

2
提示2:如果您调用[someView.superView layoutIfNeeded]然后可能会在viewWillAppear()中工作;)
Yaro

Answers:


30

包含UITextView的视图setBounds将由AutoLayout 分配其大小。所以,这就是我所做的。最初,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
您也可以在UITextViewDelegate的
textViewDidChange

2
不错的解决方案。请注意,您还可以在Interface Builder中创建约束,并使用插座将其连接。结合使用textViewDidChange此功能应节省一些麻烦的子类化和编写代码……
Bob Vork 2014年

3
请记住,textViewDidChange:以编程方式将文本添加到UITextView时不会调用该方法。
2015年

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

为此,请在“界面生成器”中选择文本视图,取消选中“属性”检查器中的“启用滚动”,然后手动添加约束。

注意:如果在文本视图上方/下方有其他视图,请考虑使用a UIStackView来排列所有视图。


12
适用于我(iOS 7 @ xcode 5)。需要注意的是,在IB中取消选中“启用滚动”将无济于事。您必须以编程方式执行此操作。
肯尼斯·姜

10
如何在文本视图上设置最大高度,然后在其内容超过文本视图的高度后使文本视图滚动?
ma11hew28'2013/

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!)
Nik Kov

3
这个答案是金。如果禁用IB中的复选框,则使用Xcode 9也可以使用。
生物

48

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

在尺寸检查器中:

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

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

在此处输入图片说明

在属性检查器中:

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

这很完美。我没有设定高度。仅需要将UItextview的垂直压缩阻力的优先级提高到1000。UItextView具有一些插图,这就是为什么自动布局会产生错误,因为禁用滚动时,即使UItextview中没有内容,UItextview也需要至少32高度。
nr5

更新:如果希望高度最小为20或其他任何值,则需要高度限制。否则,将32视为最小高度。
nr5

@Nil很高兴听到它的帮助。
Michael Revlis'9

@MichaelRevlis,这很好用。但是当有大文本时,则不会显示文本。您可以选择文本进行其他处理,但看不到。你能帮我吗 ?当我取消选中启用滚动选项时,就会发生这种情况。当我使textview滚动启用时,它可以完美显示,但我希望文本视图不滚动。
Bhautik Ziniya

@BhautikZiniya,您是否尝试过在真实设备上构建应用程序?我认为这可能是显示错误的模拟器。我有一个文本大小为100的UITextView。它在真实设备上可以正常显示,但在模拟器上看不到文本。
Michael Revlis,2017年

38

UITextView不提供internalContentSize,因此您需要对其进行子类化并提供一个。要使其自动增长,请在layoutSubviews中使nativeContentSize无效。如果您使用默认的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

1
这对我来说很好。另外,我会在Interface Builder中将此Text View的'scrollEnabled'设置为NO,因为它已经显示了所有内容而无需滚动。更重要的是,如果它位于实际的滚动视图中,并且您未在文本视图本身上禁用滚动,则可能会遇到光标在文本视图底部附近的跳跃。
idStar 2013年

这很好。有趣的是,setScrollEnabled:将其始终设置为覆盖时,NO对于不断增长的固有内容大小,您将获得一个无限循环。
Pascal

在iOS 7.0上,我还需要在每次按键后调用Auto Layout [textView sizeToFit]textViewDidChange:委托回调,以增大或缩小textview(并重新计算所有相关的约束)。
2014年

6
在这似乎不再是必要的iOS 7.1要修复并提供向后兼容性,包裹在条件-(void)layoutSubviews和包装所有的代码-(CGSize)intrinsicContentSizeversion >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
大卫·詹姆斯

1
@DavidJames这在ios 7.1.2中仍然是必需的,我不知道您要在哪里测试。
Softlion

27

您可以通过情节提要进行操作,只需禁用“已启用滚动”即可:)

故事板


2
这个问题的最佳答案
Ivan Byelko

当您使用自动版面设计时,这是最佳的解决方案。
JanApotheker

1
自动版面设置(前导,尾随,顶部,底部,无高度/宽度)+禁用滚动。然后你去。谢谢!
Farhan Arshad

15

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


7

您也可以无需子类化UITextView。看一下我如何在iOS 7上将UITextView调整为其内容的大小?

使用此表达式的值:

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

更新constant了的textView的高度UILayoutConstraint


3
您在哪里执行此操作,并且还要执行其他操作以确保UITextView均已设置?我正在执行此操作,并且文本视图本身具有大小,但是文本本身在UITextView高度的一半处被截断。在进行上述操作之前,将以编程方式设置文本。
chadbag 2013年

如果要手动设置宽度,我发现此方法比scrollEnabled = NO技巧更有用。
jchnxu 2014年

3

需要注意的重要事项:

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

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

因此,如果您在使用自动布局和文本视图时遇到问题,请尝试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 scrollview了解scrollview


将这三种方法结合在一起,您将很容易理解,您需要允许textView 的固有contentSize沿着textView的AutoLayout约束来驱动逻辑。就像您在使用textView一样,就像UILabel一样

为此,您需要禁用滚动,这基本上意味着scrollView的大小,contentSize的大小,并且如果添加containerView,则containerView的大小将全部相同。当它们相同时,您不会滚动。而且你会有。拥有意味着您还没有向下滚动。甚至没有下降1点!结果,textView将全部拉伸。0 contentOffset0 contentOffSet

它也一文不值,这 意味着scrollView的边界和框架是相同的。如果您向下滚动5点,则contentOffset将为,而您将等于0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5


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

0

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

如果你的TextView的文本弹跳起来,在编辑过程下来,经过设置[textView setScrollEnabled:NO];,设置Size Inspector > Scroll View > Content Insets > Never

希望能帮助到你。


0

将隐藏的UILabel放在textview下方。标签线=0。将UITextView的约束设置为等于UILabel(centerX,centerY,宽度,高度)。即使您离开textView的滚动行为,它仍然有效。


-1

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

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

解决方案:在子类中覆盖setBounds:。由于某些未知的原因,粘贴导致bounds.origin.y值变为非Zee(在我看到的每种情况下均为33)。因此,我将setBounds:覆盖为始终将bounds.origin.y设置为零。解决了问题。


-1

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

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

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.