我有一个完全使用自动布局以编程方式布局的视图。我在视图中间有一个 UITextView,上面和下面都有项目。一切正常,但我希望能够在添加文本时扩展 UITextView。随着它的扩展,这应该会将其下方的所有内容向下推。
我知道如何以“弹簧和支柱”的方式做到这一点,但是有没有这样做的自动布局方式?我能想到的唯一方法是在每次需要增长时删除并重新添加约束。
我有一个完全使用自动布局以编程方式布局的视图。我在视图中间有一个 UITextView,上面和下面都有项目。一切正常,但我希望能够在添加文本时扩展 UITextView。随着它的扩展,这应该会将其下方的所有内容向下推。
我知道如何以“弹簧和支柱”的方式做到这一点,但是有没有这样做的自动布局方式?我能想到的唯一方法是在每次需要增长时删除并重新添加约束。
摘要:禁用文本视图的滚动,并且不要限制它的高度。
要以编程方式执行此操作,请将以下代码放入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
将它们全部排列。
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
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];
}
我发现在您可能仍需要将 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 的占位符文本。
你也可以在没有子类化的情况下做到这一点UITextView
。看看我对如何在 iOS 7 上根据其内容调整 UITextView 大小的回答?
使用此表达式的值:
[textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height
更新constant
的textView
高度UILayoutConstraint
。
我看到多个答案建议简单地关闭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.
}
}
这意味着您只需要确保文本视图的宽度受到足够的限制,然后您就可以使用内在的内容高度(通过自动布局内容拥抱/压缩阻力优先级或在手动布局期间直接使用该值)。
不幸的是,没有记录这种行为。苹果本可以很容易地为我们省去一些麻烦……不需要额外的高度限制、子类化等。
这是一个非常重要的评论
理解为什么维生素水的答案有效的关键是三件事:
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
contentOffset
0
contentOffSet
这也毫无价值,这 意味着滚动视图的边界和框架是相同的。如果您向下滚动 5 个点,那么您的 contentOffset 将是,而您的将等于0
contentOffset
5
scrollView.bounds.origin.y - scrollView.frame.origin.y
5
我需要一个文本视图,它会自动增长到某个最大高度,然后变成可滚动的。迈克尔林克的回答很好,但我想看看我是否能想出一些更简单的东西。这是我想出的:
斯威夫特 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()
}
}
}
需要注意的重要一点:
由于 UITextView 是 UIScrollView 的子类,它受制于 UIViewController 的 automaticAdjustsScrollViewInsets 属性。
如果您正在设置布局,并且 TextView 是 UIViewControllers 层次结构中的第一个子视图,那么如果 automaticAdjustsScrollViewInsets 为 true,它将修改其 contentInsets,有时会导致自动布局中出现意外行为。
因此,如果您在自动布局和文本视图方面遇到问题,请尝试automaticallyAdjustsScrollViewInsets = false
在视图控制器上进行设置或在层次结构中向前移动 textView。
就像自动布局一样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]));
}
维生素水的答案对我有用。
如果您的 textview 的文本在编辑过程中上下弹跳,在设置之后[textView setScrollEnabled:NO];
,设置Size Inspector > Scroll View > Content Insets > Never
.
希望能帮助到你。
将隐藏的 UILabel 放置在您的文本视图下方。Label lines = 0. 将 UITextView 的约束设置为等于 UILabel (centerX, centerY, width, height)。即使您离开 textView 的滚动行为也可以工作。
顺便说一句,我使用子类构建了一个扩展的 UITextView 并覆盖了内在内容大小。我在 UITextView 中发现了一个错误,您可能想在自己的实现中进行调查。这是问题所在:
如果您一次键入单个字母,扩展的文本视图将缩小以适应不断增长的文本。但是,如果您将一堆文本粘贴到其中,它不会向下增长,但文本会向上滚动并且顶部的文本不在视线范围内。
解决方案:在您的子类中覆盖 setBounds:。由于某些未知原因,粘贴导致 bounds.origin.y 值不是 zee (在我看到的每种情况下都是 33)。所以我覆盖了 setBounds: 总是将 bounds.origin.y 设置为零。修复了问题。
这是一个快速的解决方案:
如果您将 textview 的 clipsToBounds 属性设置为 false,则可能会出现此问题。如果您只是删除它,问题就会消失。
myTextView.clipsToBounds = false //delete this line
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