UITableView 布局在 push segue 和 return 上搞砸了。 (iOS 8、Xcode beta 5、Swift)

Posted

技术标签:

【中文标题】UITableView 布局在 push segue 和 return 上搞砸了。 (iOS 8、Xcode beta 5、Swift)【英文标题】:UITableView layout messing up on push segue and return. (iOS 8, Xcode beta 5, Swift) 【发布时间】:2014-08-09 17:07:33 【问题描述】:

tldr;自动约束似乎在 push segue 时中断并返回到自定义单元格的视图

编辑:我提供了一个 github 示例项目,展示了发生的错误 https://github.com/Matthew-Kempson/TableViewExample.git

我正在创建一个应用程序,它需要自定义 UITableCell 的标题标签,以允许根据帖子标题的长度改变行。单元格正确加载到视图中,但是如果我按下一个单元格以将推送中的帖子加载到包含 WKWebView 的视图中,您可以看到,如屏幕截图所示,单元格立即移动到不正确的位置。通过 UINavigationController 的后退按钮加载视图时也会看到这一点。

在这个特定的示例中,我按下了最后一个单元格,标题为“我在巴黎拍了一张照片的两个伙伴”,所有内容都正确加载。然后如下一个屏幕截图所示,在加载第二个视图的背景下,单元格都出于未知原因向上移动。然后,当我重新加载视图时,您可以看到屏幕略微向上移动,并且我实际上无法滚动低于显示的位置。当视图加载回底部单元格下方没有消失的空白区域时,这似乎与其他测试一样是随机的。

我还附上了一张图片,其中包含单元格所具有的约束。

图片(我显然需要更多的声誉才能在这个问题中提供图片,所以它们在这个 imgur 相册中):http://imgur.com/a/gY87E

我的代码:

自定义单元格中允许单元格在旋转时正确调整视图大小的方法:

override func layoutSubviews() 
    super.layoutSubviews()

    self.contentView.layoutIfNeeded()

    // Update the label constaints
    self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.width
    self.detailsLabel.preferredMaxLayoutWidth = self.detailsLabel.frame.width

表格视图中的代码

override func viewDidLoad() 
    super.viewDidLoad()

    // Create and register the custom cell
    self.tableView.estimatedRowHeight = 56
    self.tableView.rowHeight = UITableViewAutomaticDimension

创建单元格的代码

    override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! 
    if let cell = tableView.dequeueReusableCellWithIdentifier("LinkCell", forIndexPath: indexPath) as? LinkTableViewCell 

        // Retrieve the post and set details
        let link: Link = self.linksArray.objectAtIndex(indexPath.row) as Link

        cell.titleLabel.text = link.title
        cell.scoreLabel.text = "\(link.score)"
        cell.detailsLabel.text = link.stringCreatedTimeIntervalSinceNow() + " ago by " + link.author + " to /r/" + link.subreddit

        return cell
    

    return nil

如果您需要更多代码或信息,请询问,我将提供必要的内容

感谢您的帮助!

【问题讨论】:

我遇到了类似的问题。你能解决问题吗? 我也遇到了这个问题,但我的限制在代码中。 您正在使用自动调整大小的表格视图单元格,我发现这在测试版中存在很大问题(正如在测试版中所预期的那样;自动调整大小的集合视图单元格更糟糕)。你的问题的标题说你正在使用 beta5;您的示例项目似乎在 beta6 中运行得更好(尽管仍然不完美)。 检查@POB 的回答,是的,我同意 beta 6 略有改进,但仍不如预期 我在 xCode 6.1 中看到了同样的问题。 【参考方案1】:

这个错误是由于没有tableView:estimatedHeightForRowAtIndexPath: 方法造成的。它是UITableViewDelegate 协议的可选部分。

这不是它应该如何工作的。苹果的文档说:

在加载表格视图时提供行高估计可以改善用户体验。如果表格包含可变高度行,计算所有高度可能会很昂贵,因此会导致更长的加载时间。使用估算可以将几何计算的一些成本从加载时间推迟到滚动时间。

所以这个方法应该是可选的。你会认为如果你跳过它,它会回到准确的tableView:heightForRowAtIndexPath:,对吧?但是如果你在 ios 8 上跳过它,你会得到这种行为。

似乎发生了什么?我没有内部知识,但看起来如果您不实现此方法,UITableView 会将其视为估计的行高0。它会在一定程度上弥补这一点(至少在某些情况下,会在日志中抱怨),但您仍然会看到不正确的大小。这显然是 UITableView 中的一个错误。您会在 Apple 的一些应用程序中看到此错误,包括设置等基本应用程序。

那么你如何解决它?提供方法!实施tableView: estimatedHeightForRowAtIndexPath:。如果您没有更好(和更快)的估计,只需返回UITableViewAutomaticDimension。这将完全修复此错误。

像这样:

- (CGFloat)tableView:(UITableView *)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath 
    return UITableViewAutomaticDimension;

有潜在的副作用。您提供了一个非常粗略的估计。如果您看到由此产生的后果(可能在您滚动时单元格会改变大小),您可以尝试返回更准确的估计值。 (但请记住:估计。)

也就是说,这个方法不应该返回一个完美的尺寸,只是一个足够好的尺寸。速度比准确性更重要。虽然我在模拟器中发现了一些滚动故障,但我在实际设备(iPhone 或 iPad)上的任何应用程序中都没有。 (我实际上尝试写一个更准确的估计值。但是很难平衡速度和准确性,而且我的任何应用程序都没有明显的差异。它们都工作得和返回 UITableViewAutomaticDimension 一样好,这更简单并且是足以修复错误。)

所以我建议你不要尝试做更多,除非需要更多。如果不需要,做更多的事情比修复它们更有可能导致错误。在某些情况下,您最终可能会返回 0,并且取决于您何时返回它,这可能会导致原来的问题再次出现。

上面 Kai 的答案似乎有效的原因是它实现了tableView:estimatedHeightForRowAtIndexPath:,从而避免了0 的假设。当视图消失时,它不会返回0。也就是说,Kai 的回答过于复杂、缓慢,而且并不比仅仅返回UITableViewAutomaticDimension 更准确。 (但是,再次感谢凯。如果我没有看到你的答案并受到启发将它拆开并弄清楚它为什么起作用,我永远不会明白这一点。)]

请注意,您可能还需要强制单元格布局。您可能认为 iOS 会在您返回单元格时自动执行此操作,但并非总是如此。 (一旦我进行更多调查以确定您何时需要这样做,我将对其进行编辑。)

如果您需要这样做,请在return cell;之前使用此代码:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];

【讨论】:

我也注意到了设备上的问题。初步测试表明这可以解决我的问题。感谢您的回答。 为了让它工作,我必须返回一个值,而不是 UITableViewAutomaticDimension。返回的值必须略大于我的单元格最大高度,以防止单元格在推送时崩溃。 从估计方法来看,这不应该是必要的。很好奇那里发生了什么。 欣赏答案,但我仍然发现在点击视图以推送到新视图控制器时,表格视图的滚动位置会发生变化。这应该解决这个问题吗?我在我的设备上尝试过,使用这种方法滚动似乎仍然非常不可靠。 您是否设置了estimatedRowHeight 属性?如果是,请评论它。【参考方案2】:

这种行为的问题是,当您按下 segue 时,tableView 将为可见单元格调用 estimatedHeightForRowAtIndexPath 并将单元格高度重置为默认值。这发生在viewWillDisappear 调用之后。如果你回到 TableView 所有可见的单元格都搞砸了..

我用estimatedCellHeightCache 解决了这个问题。我只是将这段代码添加到cellForRowAtIndexPath 方法中:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    ...
    // put estimated cell height in cache if needed
    if (![self isEstimatedRowHeightInCache:indexPath]) 
        CGSize cellSize = [cell systemLayoutSizeFittingSize:CGSizeMake(self.view.frame.size.width, 0) withHorizontalFittingPriority:1000.0 verticalFittingPriority:50.0];
        [self putEstimatedCellHeightToCache:indexPath height:cellSize.height];
    
    ...

现在您必须按如下方式实现estimatedHeightForRowAtIndexPath

-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath 
    return [self getEstimatedCellHeightFromCache:indexPath defaultHeight:41.5];

配置缓存

将此属性添加到您的 .h 文件中:

@property NSMutableDictionary *estimatedRowHeightCache;

实现 put/get/reset.. 缓存的方法:

#pragma mark - estimated height cache methods

// put height to cache
- (void) putEstimatedCellHeightToCache:(NSIndexPath *) indexPath height:(CGFloat) height 
    [self initEstimatedRowHeightCacheIfNeeded];
    [self.estimatedRowHeightCache setValue:[[NSNumber alloc] initWithFloat:height] forKey:[NSString stringWithFormat:@"%d", indexPath.row]];


// get height from cache
- (CGFloat) getEstimatedCellHeightFromCache:(NSIndexPath *) indexPath defaultHeight:(CGFloat) defaultHeight 
    [self initEstimatedRowHeightCacheIfNeeded];
    NSNumber *estimatedHeight = [self.estimatedRowHeightCache valueForKey:[NSString stringWithFormat:@"%d", indexPath.row]];
    if (estimatedHeight != nil) 
        //NSLog(@"cached: %f", [estimatedHeight floatValue]);
        return [estimatedHeight floatValue];
    
    //NSLog(@"not cached: %f", defaultHeight);
    return defaultHeight;


// check if height is on cache
- (BOOL) isEstimatedRowHeightInCache:(NSIndexPath *) indexPath 
    if ([self getEstimatedCellHeightFromCache:indexPath defaultHeight:0] > 0) 
        return YES;
    
    return NO;


// init cache
-(void) initEstimatedRowHeightCacheIfNeeded 
    if (self.estimatedRowHeightCache == nil) 
        self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
    


// custom [self.tableView reloadData]
-(void) tableViewReloadData 
    // clear cache on reload
    self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
    [self.tableView reloadData];

【讨论】:

这太过分了。重要的是tableView: estimatedHeightForRowAtIndexPath: 存在;它返回什么并不重要。请看下面我的回答。 (但感谢您发布此内容,否则我永远不会想到这一点!):) 嘿@Kai,您的解决方案是唯一解决我们从 segue 返回后跳动(向上)滚动问题的方法。我们在 cocoapod 中创建了一个通用的解决方案,灵感来自您的代码github.com/IndieGoGo/IGTableViewEstimatedHeightCache。 我强烈反对这种做法太过分了。可以确认在 iOS8 的导航堆栈上推送新视图控制器时,在纯代码(无笔尖/故事板)中使用 tableview 调用了估计的HeightForRowAtIndexPath 委托方法。但是,我在 iOS7 中没有看到这种行为。提供更精确的估计可以消除报告的“生涩”行为。 缓存可能应该在模型对象上键入,而不是在索引路径上。如果您在索引 0 处插入一行,您最终会为表中的每一行返回错误的高度 谢谢谢谢谢谢!一直在处理这个问题。甚至 Apple 都告诉我他们没有解决方案。【参考方案3】:

我遇到了完全相同的问题。表格视图有几个不同的单元格类,每一个都有不同的高度。此外,其中一个单元格类别必须显示额外的文本,这意味着进一步的变化。

在大多数情况下,滚动都是完美的。但是,问题中描述的相同问题表现出来了。也就是说,在选择了一个表格单元格并呈现另一个视图控制器后,在返回原始表格视图时,向上滚动非常生涩。

调查的第一线是考虑为什么要重新加载数据。经过实验,我可以确认在返回表视图时,数据被重新加载,尽管没有使用reloadData

查看我的评论ios 8 tableview reloads automatically when view appears after pop

由于没有禁用此行为的机制,下一个方法是调查生涩的滚动。

我得出的结论是,estimatedHeightForRowAtIndexPath 返回的估计值是一个估计的预先计算。登录以控制台输出估计值,您将看到当表视图首次出现时,每行都查询委托方法。那是在任何滚动之前。

我很快发现我的代码中的一些高度估计逻辑非常错误。解决这个问题可以解决最糟糕的问题。

为了实现完美的滚动,我对上面的答案采取了稍微不同的方法。高度已缓存,但使用的值来自用户向下滚动时捕获的实际高度:

    var myRowHeightEstimateCache = [String:CGFloat]()

存储:

func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) 
    myRowHeightEstimateCache["\(indexPath.row)"] = CGRectGetHeight(cell.frame)

从缓存中使用:

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat

    if let height = myRowHeightEstimateCache["\(indexPath.row)"]
    
        return height
     
    else 
    
    // Not in cache
    ... try to figure out estimate 
    

请注意,在上述方法中,您需要返回一些估计值,因为该方法当然会在 didEndDisplayingCell 之前调用。

我的猜测是,所有这些都存在某种 Apple 漏洞。这就是为什么这个问题只出现在退出场景中的原因。

底线是此解决方案与上述解决方案非常相似。但是,我避免了任何棘手的计算,并利用UITableViewAutomaticDimension 行为来缓存使用didEndDisplayingCell 显示的实际行高。

TLDR:通过缓存实际行高来解决最有可能是 UIKit 缺陷的问题。然后查询你的缓存作为估计方法中的第一个选项。

【讨论】:

将此与 var myRowHeightEstimateCache = [NSIndexPath:CGFloat]() 一起使用,您也可以支持部分 显然是迄今为止最好的解决方案。让我们希望 Apple 在不久的将来解决这个问题。谢谢麦克斯! @f0rz 不用担心!很高兴提供帮助:-) 确保在生命周期方法中删除对self.tableView.reloadData() 的任何无关调用(即viewWillAppearviewDidAppear...)。我遇到了同样的问题,这是由于在viewDidAppear() 中对self.tableView.reloadData() 进行了额外不必要的调用。【参考方案4】:

好吧,在它起作用之前,你可以删除这两行:

self.tableView.estimatedRowHeight = 45
self.tableView.rowHeight = UITableViewAutomaticDimension

并将此方法添加到您的 viewController:

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat 
    let cell = tableView.dequeueReusableCellWithIdentifier("cell") as TableViewCell
    cell.cellLabel.text = self.tableArray[indexPath.row]

    //Leading space to container margin constraint: 0, Trailling space to container margin constraint: 0
    let width = tableView.frame.size.width - 0
    let size = cell.cellLabel.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))

    //Top space to container margin constraint: 0, Bottom space to container margin constraint: 0, cell line: 1
    let height = size.height + 1

    return (height <= 45) ? 45 : height

它在您的测试项目中没有任何其他更改即可工作。

【讨论】:

工作完美,符合预期,但我确信在未来的 Beta 版中它有望得到修复! 我如何计算一个包含两个标签的单元格的高度,一个在一个之上。顶部单元格可调整大小,仅底部单行。我尝试过但没有成功。谢谢@POB【参考方案5】:

如果你设置了tableView的estimatedRowHeight属性。

tableView.estimatedRowHeight = 100;

然后评论它。

//  tableView.estimatedRowHeight = 100;

它为我解决了 iOS8.1 中出现的错误。

如果你真的想保留它,那么你可以强制tableView在推送之前重新加载数据。

[self.tableView reloadData];
[self.navigationController pushViewController:vc animated:YES];

viewWillDisappear:进行。

- (void)viewWillDisappear:(BOOL)animated 
    [super viewWillDisappear:animated];
    [self.tableView reloadData];

希望对你有帮助。

【讨论】:

【参考方案6】:

在我的 xcode 6 final 中,解决方法不起作用。我正在使用自定义单元格并将 heightForCell 中的单元格出队导致无限循环。将单元格出列时调用 heightForCell。

而且 bug 似乎仍然存在。

【讨论】:

有趣的是,这不会发生在我身上? 我不确定解决方法是否有效,但根据您的描述,您调用的是 dequeueReusableCellWithIdentifier:forIndexPath: 而不是 dequeueReusableCellWithIdentifier:【参考方案7】:

如果上述方法都不适合您(就像我遇到的那样),只需检查表视图中的 estimatedRowHeight 属性是否准确。当它实际上接近 150 像素时,我检查了我使用的是 50 像素。更新这个值解决了这个问题!

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = tableViewEstimatedRowHeight // This should be accurate.

【讨论】:

以上是关于UITableView 布局在 push segue 和 return 上搞砸了。 (iOS 8、Xcode beta 5、Swift)的主要内容,如果未能解决你的问题,请参考以下文章

iOS Objective-C:如何从另一个类调用 Push Segue

如果未关闭键盘,则在使用搜索栏时进行 segueing 会更改目标视图控制器布局

如何在具有自动布局的容器视图中将 UITableView 的大小设置为子视图

通过 segue 问题在 UITableView 和 UIViewController 之间推送数据

详细视图 UITableView - 在 segue 之前传递数据

在 unwind segue 之后执行 push segue