在键入时调整包含 UITextView 的 UITableViewCell 的大小
Resize UITableViewCell containing UITextView upon typing
我有一个包含自定义单元格的 UITableViewController。每个单元格都是使用 nib 创建的,并包含一个不可滚动的 UITextView。我在每个单元格内添加了约束,以便单元格调整其高度以适应 UITextView 的内容。所以最初我的控制器看起来像这样:
现在我希望当用户在单元格中键入内容时,其内容会自动适应。这个问题已经被问过很多次了,具体见this or the second answer here。因此,我在我的代码中编写了以下委托:
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
[self.tableView beginUpdates];
[self.tableView endUpdates];
return YES;
}
然而,它会导致以下奇怪的行为:所有约束都被忽略,所有单元格高度都折叠到最小值。见下图:
如果我上下滚动 tableView 以强制调用新的 cellForRowAtIndexPath,我会恢复单元格的正确高度:
请注意,我没有实现 heightForRowAtIndexPath,因为我希望 autoLayout 能够解决这个问题。
有人可以告诉我我做错了什么或帮助我吗?非常感谢!
我已经使用 UITextView 实现了类似的方法,但是为此我必须实现 heightForRowAtIndexPath
#pragma mark - SizingCell
- (USNTextViewTableViewCell *)sizingCell
{
if (!_sizingCell)
{
_sizingCell = [[USNTextViewTableViewCell alloc] initWithFrame:CGRectMake(0.0f,
0.0f,
self.tableView.frame.size.width,
0.0f)];
}
return _sizingCell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
self.sizingCell.textView.text = self.profileUpdate.bio;
[self.sizingCell setNeedsUpdateConstraints];
[self.sizingCell updateConstraintsIfNeeded];
[self.sizingCell setNeedsLayout];
[self.sizingCell layoutIfNeeded];
CGSize cellSize = [self.sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
sizingCell 是仅用于尺寸计算的单元格实例。
需要注意的是,您需要将 UITextView 的上下边缘附加到 UITableViewCells contentView 的上下边缘,以便随着 UITableViewCell 高度的变化,UITextView 的高度也会发生变化。
对于约束布局,我使用了 PureLayout (https://github.com/smileyborg/PureLayout),因此以下约束布局代码对您来说可能不常见:
#pragma mark - Init
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self)
{
[self.contentView addSubview:self.textView];
}
return self;
}
#pragma mark - AutoLayout
- (void)updateConstraints
{
[super updateConstraints];
/*-------------*/
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeLeft
withInset:10.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop
withInset:5.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom
withInset:5.0f];
[self.textView autoSetDimension:ALDimensionWidth
toSize:200.0f];
}
以下示例适用于用户在单元格中键入文本时的动态行高。即使您使用自动布局,您仍然必须实现 heightForRowAtIndexPath 方法。要使此示例正常工作,必须以这样的方式将约束设置为 textView,即如果单元格高度增加,textView 的高度也会增加。这可以通过从 textView 到单元格内容视图添加顶部约束和底部约束来实现。但不要为 textView 本身设置高度限制。还为 textView 启用滚动,以便 textView 的内容大小将在用户输入文本时更新。然后我们使用这个内容大小来计算新的行高。只要行高足够长以垂直拉伸 textView 使其等于或大于其内容大小,即使启用了滚动,文本视图也不会滚动,我相信这就是您所需要的。
在这个例子中,我只有一行,而且我只使用一个变量来跟踪行高。但是当我们有多行时,我们需要为每一行设置一个变量,否则所有行的高度都将相同。在这种情况下可以使用与tableView数据源数组对应的rowHeight数组。
@interface ViewController ()
@property (nonatomic, assign)CGFloat rowHeight;;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.rowHeight = 60;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell1"];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.rowHeight;
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates];
CGFloat paddingForTextView = 40; //Padding varies depending on your cell design
self.rowHeight = textView.contentSize.height + paddingForTextView;
[self.tableView endUpdates];
}
@end
在前面两个答案的启发下,我找到了解决问题的方法。我认为我有一个 UITextView 的事实给 autoLayout 带来了一些麻烦。我在原始代码中添加了以下两个函数。
- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont systemFontOfSize:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.sampleStrings[indexPath.row] attributes:attrsDictionary];
return [self textViewHeightForAttributedText:attrString andWidth:CGRectGetWidth(self.tableView.bounds)-31]+20;
}
最后一行 31
是我对单元格左侧和右侧的约束总和,20
只是一些任意松弛。
我在阅读这篇文章时找到了这个解决方案 this very interesting answer。
这是一个 swift 解决方案,对我来说效果很好。如果您使用自动布局,则需要为 estimatedRowHeight 分配一个值,然后为行高分配 return UITableViewAutomaticDimension。最后在文本视图委托中执行类似于下面的操作。
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// MARK: UITextViewDelegate
func textViewDidChange(textView: UITextView) {
// Calculate if the text view will change height, then only force
// the table to update if it does. Also disable animations to
// prevent "jankiness".
let startHeight = textView.frame.size.height
let calcHeight = textView.sizeThatFits(textView.frame.size).height //iOS 8+ only
if startHeight != calcHeight {
UIView.setAnimationsEnabled(false) // Disable animations
self.tableView.beginUpdates()
self.tableView.endUpdates()
// Might need to insert additional stuff here if scrolls
// table in an unexpected way. This scrolls to the bottom
// of the table. (Though you might need something more
// complicated if editing in the middle.)
let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
使用 Swift 2.2(较早的版本也可能有效),如果您将 TableView 设置为使用自动尺寸(假设您在子类 UITableViewController
中工作,如下所示:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50 // or something
您只需要在此文件中实现委托,UITextViewDelegate
,并添加以下函数,它应该可以工作。请记住将您的 textView 的委托设置为 self
(因此,也许在您将单元格出列后,cell.myTextView.delegate = self
)
func textViewDidChange(textView: UITextView) {
self.tableView.beginUpdates()
textView.frame = CGRectMake(textView.frame.minX, textView.frame.minY, textView.frame.width, textView.contentSize.height + 40)
self.tableView.endUpdates()
}
感谢 "Jose Tomy Joseph" 对这个答案的启发(实际上是支持)。
我的解决方案与@atlwx 类似,但更短一些。使用静态 table 进行测试。需要 UIView.setAnimationsEnabled(false)
来防止单元格的内容 "jumping" 而 table 更新该单元格的高度
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func textViewDidChange(_ textView: UITextView) {
UIView.setAnimationsEnabled(false)
textView.sizeToFit()
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
查看我在下面 link 中提供的 Objective C 解决方案。
易于实施,干净,无需自动布局。无需约束。在 iOS10 和 iOS11.
中测试
在不关闭键盘的情况下立即以平滑的方式更新 tableview 单元格高度的技巧是 运行 在设置 textView 或其他内容的大小后,在 textViewDidChange 事件中调用以下代码片段你在单元格中有:
[tableView beginUpdates];
[tableView endUpdates];
然而,这可能还不够。您还应该确保 tableView 具有足够的弹性以保持相同的 contentOffset。您可以通过设置 tableView contentInset 底部来获得这种弹性。我建议这个弹性值至少是你需要的从最后一个单元格底部到 tableView 底部的最大距离。例如,它可以是键盘的高度。
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
有关此问题的更多详细信息和一些有用的额外功能,请查看以下内容 link:
几乎每个人都建议的解决方案是要走的路,我只会添加一个小的改进。作为回顾:
只需设置估计高度,我通过故事板来完成:
确保您在单元格中正确设置了 UITextView 的约束。
- 在
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
我简单地调用:
cell.myTextView.sizeToFit()
测试于 iOS 12
我真的尝试了很多的解决方案,终于找到了一个好的here
这 有动画效果 并且看起来很漂亮。诀窍是 DispatchQueue.async
块。
我还使用 TPKeyboardAvoidingTableView
来确保键盘不会重叠任何东西。
func textViewDidChange(_ textView: UITextView) {
// Animated height update
DispatchQueue.main.async {
self.tableView?.beginUpdates()
self.tableView?.endUpdates()
}
}
更新
由于 TPKeyboardAvoidingTableView
,我遇到了奇怪的跳跃问题。特别是当我滚动到底部然后 UITextView
激活时。
所以我用原生 UITableView
替换了 TPKeyboardAvoidingTableView
并自己处理插图。 table 视图是本地滚动的。
以前 beginUpdates
/endUpdates
是广告解决方案。
自 iOS 11 以来,performBatchUpdates
已被推荐 source。
我可以在更改单元格内容后调用 performBatchUpdates
。
我有一个包含自定义单元格的 UITableViewController。每个单元格都是使用 nib 创建的,并包含一个不可滚动的 UITextView。我在每个单元格内添加了约束,以便单元格调整其高度以适应 UITextView 的内容。所以最初我的控制器看起来像这样:
现在我希望当用户在单元格中键入内容时,其内容会自动适应。这个问题已经被问过很多次了,具体见this or the second answer here。因此,我在我的代码中编写了以下委托:
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
[self.tableView beginUpdates];
[self.tableView endUpdates];
return YES;
}
然而,它会导致以下奇怪的行为:所有约束都被忽略,所有单元格高度都折叠到最小值。见下图:
如果我上下滚动 tableView 以强制调用新的 cellForRowAtIndexPath,我会恢复单元格的正确高度:
请注意,我没有实现 heightForRowAtIndexPath,因为我希望 autoLayout 能够解决这个问题。
有人可以告诉我我做错了什么或帮助我吗?非常感谢!
我已经使用 UITextView 实现了类似的方法,但是为此我必须实现 heightForRowAtIndexPath
#pragma mark - SizingCell
- (USNTextViewTableViewCell *)sizingCell
{
if (!_sizingCell)
{
_sizingCell = [[USNTextViewTableViewCell alloc] initWithFrame:CGRectMake(0.0f,
0.0f,
self.tableView.frame.size.width,
0.0f)];
}
return _sizingCell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
self.sizingCell.textView.text = self.profileUpdate.bio;
[self.sizingCell setNeedsUpdateConstraints];
[self.sizingCell updateConstraintsIfNeeded];
[self.sizingCell setNeedsLayout];
[self.sizingCell layoutIfNeeded];
CGSize cellSize = [self.sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
sizingCell 是仅用于尺寸计算的单元格实例。
需要注意的是,您需要将 UITextView 的上下边缘附加到 UITableViewCells contentView 的上下边缘,以便随着 UITableViewCell 高度的变化,UITextView 的高度也会发生变化。
对于约束布局,我使用了 PureLayout (https://github.com/smileyborg/PureLayout),因此以下约束布局代码对您来说可能不常见:
#pragma mark - Init
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self)
{
[self.contentView addSubview:self.textView];
}
return self;
}
#pragma mark - AutoLayout
- (void)updateConstraints
{
[super updateConstraints];
/*-------------*/
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeLeft
withInset:10.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop
withInset:5.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom
withInset:5.0f];
[self.textView autoSetDimension:ALDimensionWidth
toSize:200.0f];
}
以下示例适用于用户在单元格中键入文本时的动态行高。即使您使用自动布局,您仍然必须实现 heightForRowAtIndexPath 方法。要使此示例正常工作,必须以这样的方式将约束设置为 textView,即如果单元格高度增加,textView 的高度也会增加。这可以通过从 textView 到单元格内容视图添加顶部约束和底部约束来实现。但不要为 textView 本身设置高度限制。还为 textView 启用滚动,以便 textView 的内容大小将在用户输入文本时更新。然后我们使用这个内容大小来计算新的行高。只要行高足够长以垂直拉伸 textView 使其等于或大于其内容大小,即使启用了滚动,文本视图也不会滚动,我相信这就是您所需要的。
在这个例子中,我只有一行,而且我只使用一个变量来跟踪行高。但是当我们有多行时,我们需要为每一行设置一个变量,否则所有行的高度都将相同。在这种情况下可以使用与tableView数据源数组对应的rowHeight数组。
@interface ViewController ()
@property (nonatomic, assign)CGFloat rowHeight;;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.rowHeight = 60;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell1"];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.rowHeight;
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates];
CGFloat paddingForTextView = 40; //Padding varies depending on your cell design
self.rowHeight = textView.contentSize.height + paddingForTextView;
[self.tableView endUpdates];
}
@end
在前面两个答案的启发下,我找到了解决问题的方法。我认为我有一个 UITextView 的事实给 autoLayout 带来了一些麻烦。我在原始代码中添加了以下两个函数。
- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont systemFontOfSize:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.sampleStrings[indexPath.row] attributes:attrsDictionary];
return [self textViewHeightForAttributedText:attrString andWidth:CGRectGetWidth(self.tableView.bounds)-31]+20;
}
最后一行 31
是我对单元格左侧和右侧的约束总和,20
只是一些任意松弛。
我在阅读这篇文章时找到了这个解决方案 this very interesting answer。
这是一个 swift 解决方案,对我来说效果很好。如果您使用自动布局,则需要为 estimatedRowHeight 分配一个值,然后为行高分配 return UITableViewAutomaticDimension。最后在文本视图委托中执行类似于下面的操作。
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// MARK: UITextViewDelegate
func textViewDidChange(textView: UITextView) {
// Calculate if the text view will change height, then only force
// the table to update if it does. Also disable animations to
// prevent "jankiness".
let startHeight = textView.frame.size.height
let calcHeight = textView.sizeThatFits(textView.frame.size).height //iOS 8+ only
if startHeight != calcHeight {
UIView.setAnimationsEnabled(false) // Disable animations
self.tableView.beginUpdates()
self.tableView.endUpdates()
// Might need to insert additional stuff here if scrolls
// table in an unexpected way. This scrolls to the bottom
// of the table. (Though you might need something more
// complicated if editing in the middle.)
let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
使用 Swift 2.2(较早的版本也可能有效),如果您将 TableView 设置为使用自动尺寸(假设您在子类 UITableViewController
中工作,如下所示:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50 // or something
您只需要在此文件中实现委托,UITextViewDelegate
,并添加以下函数,它应该可以工作。请记住将您的 textView 的委托设置为 self
(因此,也许在您将单元格出列后,cell.myTextView.delegate = self
)
func textViewDidChange(textView: UITextView) {
self.tableView.beginUpdates()
textView.frame = CGRectMake(textView.frame.minX, textView.frame.minY, textView.frame.width, textView.contentSize.height + 40)
self.tableView.endUpdates()
}
感谢 "Jose Tomy Joseph" 对这个答案的启发(实际上是支持)。
我的解决方案与@atlwx 类似,但更短一些。使用静态 table 进行测试。需要 UIView.setAnimationsEnabled(false)
来防止单元格的内容 "jumping" 而 table 更新该单元格的高度
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func textViewDidChange(_ textView: UITextView) {
UIView.setAnimationsEnabled(false)
textView.sizeToFit()
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
查看我在下面 link 中提供的 Objective C 解决方案。 易于实施,干净,无需自动布局。无需约束。在 iOS10 和 iOS11.
中测试在不关闭键盘的情况下立即以平滑的方式更新 tableview 单元格高度的技巧是 运行 在设置 textView 或其他内容的大小后,在 textViewDidChange 事件中调用以下代码片段你在单元格中有:
[tableView beginUpdates];
[tableView endUpdates];
然而,这可能还不够。您还应该确保 tableView 具有足够的弹性以保持相同的 contentOffset。您可以通过设置 tableView contentInset 底部来获得这种弹性。我建议这个弹性值至少是你需要的从最后一个单元格底部到 tableView 底部的最大距离。例如,它可以是键盘的高度。
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
有关此问题的更多详细信息和一些有用的额外功能,请查看以下内容 link:
几乎每个人都建议的解决方案是要走的路,我只会添加一个小的改进。作为回顾:
只需设置估计高度,我通过故事板来完成:
确保您在单元格中正确设置了 UITextView 的约束。
- 在
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
我简单地调用:
cell.myTextView.sizeToFit()
测试于 iOS 12
我真的尝试了很多的解决方案,终于找到了一个好的here
这 有动画效果 并且看起来很漂亮。诀窍是 DispatchQueue.async
块。
我还使用 TPKeyboardAvoidingTableView
来确保键盘不会重叠任何东西。
func textViewDidChange(_ textView: UITextView) {
// Animated height update
DispatchQueue.main.async {
self.tableView?.beginUpdates()
self.tableView?.endUpdates()
}
}
更新
由于 TPKeyboardAvoidingTableView
,我遇到了奇怪的跳跃问题。特别是当我滚动到底部然后 UITextView
激活时。
所以我用原生 UITableView
替换了 TPKeyboardAvoidingTableView
并自己处理插图。 table 视图是本地滚动的。
以前 beginUpdates
/endUpdates
是广告解决方案。
自 iOS 11 以来,performBatchUpdates
已被推荐 source。
我可以在更改单元格内容后调用 performBatchUpdates
。