在键入时调整包含 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.

中测试

在不关闭键盘的情况下平滑地调整大小和移动 UITableViewCell

在不关闭键盘的情况下立即以平滑的方式更新 tableview 单元格高度的技巧是 运行 在设置 textView 或其他内容的大小后,在 textViewDidChange 事件中调用以下代码片段你在单元格中有:

[tableView beginUpdates];
[tableView endUpdates];

然而,这可能还不够。您还应该确保 tableView 具有足够的弹性以保持相同的 contentOffset。您可以通过设置 tableView contentInset 底部来获得这种弹性。我建议这个弹性值至少是你需要的从最后一个单元格底部到 tableView 底部的最大距离。例如,它可以是键盘的高度。

self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);

有关此问题的更多详细信息和一些有用的额外功能,请查看以下内容 link:

几乎每个人都建议的解决方案是要走的路,我只会添加一个小的改进。作为回顾:

  1. 只需设置估计高度,我通过故事板来完成:

  2. 确保您在单元格中正确设置了 UITextView 的约束。

  3. 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