【问题标题】:UITextView that expands to text using auto layout使用自动布局扩展为文本的 UITextView
【发布时间】:2013-05-27 21:56:45
【问题描述】:

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

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

【问题讨论】:

  • 你可以为文本视图做一个高度约束的IBOutlet,当你想要它增长的时候修改这个常量,不需要删除和添加。
  • 2017,一个经常让您受挫的重要提示:只有在查看后设置它才有效>出现将文本设置为 ViewDidLoad - 它不起作用!
  • tip No2:如果你调用 [someView.superView layoutIfNeeded] 那么它可能在 viewWillAppear() 中工作;)

标签: ios uitextview autolayout nslayoutconstraint


【解决方案1】:

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

要以编程方式执行此操作,请将以下代码放入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,然后手动添加约束。

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

【讨论】:

  • 为我工作(iOS 7 @ xcode 5)。需要注意的是,简单地取消选中 IB 中的“启用滚动”将无济于事。您必须以编程方式进行。
  • 如何在文本视图上设置最大高度,然后在其内容超过文本视图高度后让文本视图滚动?
  • @iOSGeek 只需创建一个 UITextView 子类并覆盖intrinsicContentSize 就像这样- (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }。告诉我它是否适合你。自己没有测试。
  • 设置这个属性,把UITextView放到UIStackView中。 Voilla - 自动调整 textView 的大小!)
  • 这个答案是黄金。对于 Xcode 9,如果您禁用 IB 中的复选框,它也可以工作。
【解决方案2】:

对于喜欢自动布局的人来说,这是一个解决方案:

在尺寸检查器中:

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

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

在属性检查器中:

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

【讨论】:

  • 这是完美的工作。我没有设置高度。只需要将 UItextview 的垂直压缩阻力的优先级提高到 1000。 UItextView 有一些插图,这就是为什么自动布局会出错,因为 UItextview 需要至少 32 高度,即使其中没​​有内容,当滚动被禁用时。
  • 更新:如果您希望高度最小为 20 或其他任何值,则需要高度约束。否则 32 将被视为最小高度。
  • @Nil 很高兴听到它有帮助。
  • @MichaelRevlis,这很好用。但是当有大文本时,文本不会显示。您可以选择文本进行其他处理,但它是不可见的。你能帮帮我吗? ?当我取消选中启用滚动选项时会发生这种情况。当我启用 textview 滚动时,它会完美显示,但我希望文本视图不滚动。
  • @BhautikZiniya,您是否尝试过在真实设备上构建您的应用程序?我认为这可能是一个显示错误的模拟器。我有一个文本字体大小为 100 的 UITextView。它在真实设备上显示良好,但在模拟器上不可见文本。
【解决方案3】:

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

【讨论】:

  • 这对我来说效果很好。此外,我会在此文本视图的 Interface Builder 中将“scrollEnabled”设置为 NO,因为它已经显示了所有内容而无需滚动。更重要的是,如果它位于实际的滚动视图中,如果您不禁用文本视图本身的滚动,您可能会在文本视图底部附近体验光标跳跃。
  • 这很好用。有趣的是,当覆盖 setScrollEnabled: 以始终将其设置为 NO 时,您将获得一个无限循环,以适应不断增长的内在内容大小。
  • 在 iOS 7.0 上,我还需要在 textViewDidChange: 委托回调中调用 [textView sizeToFit] 以在每次击键后扩大或缩小文本视图(并重新计算任何相关约束)。
  • 这在 iOS 7.1 中似乎不再需要为了修复并提供向后兼容性,请将条件包装在 -(void)layoutSubviews 中并包装 all代码在-(CGSize)intrinsicContentSizeversion >= 7.0f && version < 7.1f + else return [super intrinsicContentSize];
  • @DavidJames 这在 ios 7.1.2 中仍然是必需的,我不知道你在哪里测试。
【解决方案4】:

包含 UITextView 的视图将被 AutoLayout 分配其大小为setBounds。所以,这就是我所做的。超级视图最初设置了所有其他应有的约束,最后我为 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];
}

【讨论】:

  • 您也可以更新 UITextViewDelegate 的 'textViewDidChange:' 中的约束
  • 不错的解决方案。请注意,您还可以在 Interface Builder 中创建约束,并使用插座连接它。结合使用 textViewDidChange 这应该可以省去一些麻烦的子类化和编写代码……
  • 请记住,以编程方式向 UITextView 添加文本时不会调用textViewDidChange:
  • @BobVork 建议在 XCODE 11/iOS13 中运行良好,在 viewWillAppear 中设置文本视图值并在 textViewDidChange 中更新时设置约束常量。
【解决方案5】:

您可以通过情节提要完成,只需禁用“启用滚动”即可:)

【讨论】:

  • 这个问题的最佳答案
  • 这是使用自动布局时的最佳解决方案。
  • 自动布局设置(前导、尾随、顶部、底部、无高度/宽度)+禁用滚动。你去吧。谢谢!
【解决方案6】:

我发现在您可能仍需要将 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 的情况下这样做。看看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
    

    更新textView的高度constantUILayoutConstraint

    【讨论】:

    • 您在哪里执行此操作,您是否正在执行其他操作以确保 UITextView 已全部设置好?我正在这样做,文本视图本身的大小,但文本本身在 UITextView 的高度的一半处被切断。在执行上述操作之前以编程方式设置文本。
    • 如果你想手动设置宽度,我发现这比 scrollEnabled = NO 技巧更有用。
    【解决方案8】:

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

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

    【讨论】:

    • 我们如何使用它?
    • @lintojacob 只需将您的文本视图设为AutoExpandingTextView 而不是UITextView 并根据需要调整maxHeight
    【解决方案9】:

    我看到多个答案建议直接关闭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】:

    需要注意的重要一点:

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

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

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

    【讨论】:

      【解决方案11】:

      这更重要的评论

      了解vitaminwater's answer 为何有效的关键在于三件事:

      1. 知道UITextView是UIScrollView类的子类
      2. 了解 ScrollView 的工作原理以及它的 contentSize 是如何计算的。有关更多信息,请参阅此here 答案及其各种解决方案和 cmets。
      3. 了解 contentSize 是什么以及它是如何计算的。请参阅 herehere。设置contentOffset 也可能会有所帮助可能不过:

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

      欲了解更多信息,请参阅objc scrollviewunderstanding scrollview


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

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

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

      【讨论】:

        【解决方案12】:

        即插即用解决方案 - 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]));
        }
        

        【讨论】:

          【解决方案13】:

          vitaminwater 的答案对我有用。

          如果你的textview的文本在编辑过程中上下跳动,设置[textView setScrollEnabled:NO];后,设置Size Inspector &gt; Scroll View &gt; Content Insets &gt; Never

          希望对你有帮助。

          【讨论】:

            【解决方案14】:

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

            【讨论】:

              【解决方案15】:

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

              如果您一次键入单个字母,扩展的文本视图会缩小以适应不断增长的文本。但是如果你在里面粘贴一堆文字,它不会向下增长,但文字会向上滚动,顶部的文字就看不见了。

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

              【讨论】:

                【解决方案16】:

                这里有一个快速的解决方案:

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

                myTextView.clipsToBounds = false //delete this line
                

                【讨论】:

                  【解决方案17】:
                  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
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2011-03-30
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-05-25
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-01-19
                    • 2021-10-12
                    相关资源
                    最近更新 更多