自定义UITableview单元格偶尔会在其他单元格之上重叠/复制

5

我在这里遇到了一个非常奇怪的问题。

基本上,用户访问了一个带有TableView的视图。 有一个调用后端的数据请求,在该数据的完成块中,将数据提供给TableView来显示。

在可重用的自定义单元格的cellForRow 1中,根据一些逻辑出列一些单元格,然后调用该单元格的configure方法。

这就是它变得棘手的地方:某些单元格可能需要进一步的异步调用(以获取其自定义显示的更多数据),因此需要tableView.reloadRows调用……然后,下面两张截图最好解释了发生了什么:

错误 正确

在第一个屏幕截图中,“曾经想过为Klondike巧克力条做什么?”的那个单元格会复制到包含Twitter信息的单元格“演员自我管理”之上,由于作为包含Twitter信息的单元格,需要额外的异步调用来获取该数据。

有什么想法吗?我在begin/endUpdates块内运行tableView.reloadRows调用,但没有结果。

cellForRow:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let postAtIndexPath = posts[indexPath.row]
    let cell: PostCell!

    if postAtIndexPath.rawURL == "twitter.com" {
        cell = tableView.dequeueReusableCell(withIdentifier: "TwitterCell", for: indexPath) as! TwitterCell
    } else if !postAtIndexPath.rawURL.isEmpty {
        cell = tableView.dequeueReusableCell(withIdentifier: "LinkPreviewCell", for: indexPath) as! LinkPreviewCell
    } else if postAtIndexPath.quotedID > -1 {
        cell = tableView.dequeueReusableCell(withIdentifier: "QuoteCell", for: indexPath) as! QuoteCell
    } else {
        cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath) as! PostCell
    }

    cell.isUserInteractionEnabled = true

    cell.privacyDelegate = self
    cell.shareDelegate = self
    cell.likeDelegate = self
    cell.linkPreviewDelegate = self
    cell.reloadDelegate = self
    postToIndexPath[postAtIndexPath.id] = indexPath

    if parentView == "MyLikes" || parentView == "Trending" {
        cell.privatePostButton.isHidden = true
        cell.privatePostLabel.isHidden = true
    }

    cell.configureFor(post: postAtIndexPath,
                         liked: likesCache.postIsLiked(postId: postAtIndexPath.id),
                         postIdToAttributions: postIdToAttributions,
                         urlStringToOGData: urlStringToOGData,
                         imageURLStringToData: imageURLStringToData)

    cell.contentView.layoutIfNeeded()

    return cell
}

异步 Twitter 调用:

private func processForTwitter(og: OpenGraph, urlLessString: NSMutableAttributedString, completion:(() -> ())?) {
    let ogURL = og[.url]!
    let array = ogURL.components(separatedBy: "/")
    let client = TWTRAPIClient()
    let tweetID = array[array.count - 1]
    if let tweet = twitterCache.tweet(forID: Int(tweetID)!)  {
        for subView in containerView.subviews {
            subView.removeFromSuperview()
        }

        let tweetView = TWTRTweetView(tweet: tweet as? TWTRTweet, style: .compact)
        containerView.addSubview(tweetView)
        tweetView.translatesAutoresizingMaskIntoConstraints = false
        tweetView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
        tweetView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
        containerView.leftAnchor.constraint(equalTo: tweetView.leftAnchor).isActive = true
        containerView.rightAnchor.constraint(equalTo: tweetView.rightAnchor).isActive = true
        containerView.isHidden = false
        postText.isHidden = false

    } else {
        client.loadTweet(withID: tweetID, completion: { [weak self] (t, error) in
            if let weakSelf = self {
                if let tweet = t {
                    weakSelf.twitterCache.addTweetToCache(tweet: tweet, forID: Int(tweetID)!)
                }
            }

            if let complete = completion {
                complete()
            }
        })`enter code here`
    }

    if urlLessString.string.isEmpty {
        if let ogTitle = og[.title] {
            postText.text = String(htmlEncodedString: ogTitle)
        }
    } else {
        postText.attributedText = urlLessString
    }
}

在tableviewcontroller中的完成块:

func reload(postID: Int) {

    tableView.beginUpdates()
    self.tableView.reloadRows(at: [self.postToIndexPath[postID]!], with: .automatic)
    tableView.endUpdates()

}

NB:这并不是每次都会发生,很可能是与网络有关。


你可以尝试使用tableView.reloadData而不是tableView.reloadRows。 - Shehata Gamal
一个快速的建议 - 看了你的代码,我没有看到任何后台调用,你可以试试。将网络调用放在后台,并在主线程上重新加载表格单元。 - Kampai
5个回答

1
我想问题在于计算单元格中存在的组件的大小。就像单元格被取消引用并设置框架和子视图之前,数据是不可用的。
也许你可以尝试:
tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = yourPrefferedHeight
但你需要确保自动布局约束是正确的,并且如果所有组件都具有高度,则单元格可以确定其高度。

是的,我已经设置了自动尺寸和估计行高,但没有成功。不过我会再次检查我的约束条件,也许有问题。 - Nick Coelius
也许尝试在单元格的contentView中添加一个单独的视图,并将所有单元格子视图添加到新添加的视图上。尝试为新视图提供一些预定义的高度约束,看看是否有单元格重叠,然后可以根据每个单元格视图的高度排列高度约束。 - nikBhosale

1
原来在 estimatedRowHeightreloadRows 周围存在奇怪的行为,或者至少我不理解;通过一个相关的SO问题,解决方案是在可用时缓存行高度,并在 heightForRow 中返回它。 我猜它在重新加载时没有重新计算高度之类的东西?具体为什么这样做有效,我真的不明白,但我很高兴它能起作用!

1
在我的情况下,禁用估算高度是有效的(tableView.estimatedRowHeight=0)。在表视图上进行begin/endUpdates调用后,我遇到了重叠问题,其中单元格的高度需要稍微更改。 - alex-i
@alex-i,你的建议对我很有效,但是当我尝试将estimatedRowheight初始值设为零时,单元格中的所有内容都重叠在一起。你能帮我解决这个问题吗? - SRI
1
@SRI 我只能猜测问题所在。我建议将 tableView.rowHeight = <some_value>; 设置一下,还要确保你也实现了 -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 的代理方法。 - alex-i

1
在异步调用之后,尝试调用tableView.reloadData而不是tableView.reloadRows
tableView.beginUpdates()
tableView.setNeedsLayout()
tableView.endUpdates()

似乎数据已经被填充,但单元格的大小没有反映出来。如果调用reloadRows,恐怕它不会重新计算以下单元格的新位置 - 这意味着如果第3行单元格的高度调整为更大,它将变得更大,但下面的单元格不会被推迟。

这个...似乎正在工作!我移动了一些逻辑(之前reloadRows调用使用了我在第一个异步调用后加载的缓存),现在...它看起来很开心?至少,我还没有观察到它出现问题!谢谢! - Nick Coelius
我说得太早了 - 它仍在发生 :-/ - Nick Coelius
编辑了代码。Q现在变得很长了,哇。这是在尝试您的答案之前的代码版本,您的答案版本只需要将约束代码等移动到loadTweet回调中,然后显然添加setNeedsDisplay即可。 - Nick Coelius
setNeedsDisplay 没有意义,它仅用于自定义绘制其内容的视图。您应该使用 setNeedsLayout 替代。 - Werner Altewischer
@WernerAltewischer 双向都可以...但我猜 setNeedsLayout 可能是更高效的方法,所以我会更新答案。 - Milan Nosáľ
显示剩余5条评论

0
我假设processForTwitter是在configureFor内部从后台/异步调用的。如果是这样,那么问题出在这里。你的API调用应该在后台进行,但如果你从缓存中获取推文,那么addSubview应该在主线程上完成,并在返回单元格之前完成。尝试这样做,确保processForTwitter在主线程上调用并进行配置。
   private func processForTwitter(og: OpenGraph, urlLessString: NSMutableAttributedString, completion:(() -> ())?) {
        let ogURL = og[.url]!
        let array = ogURL.components(separatedBy: "/")
        let client = TWTRAPIClient()
        let tweetID = array[array.count - 1]
        if let tweet = twitterCache.tweet(forID: Int(tweetID)!)  {
            for subView in containerView.subviews {
                subView.removeFromSuperview()
            }

            let tweetView = TWTRTweetView(tweet: tweet as? TWTRTweet, style: .compact)
            containerView.addSubview(tweetView)
            tweetView.translatesAutoresizingMaskIntoConstraints = false
            tweetView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
            tweetView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
            containerView.leftAnchor.constraint(equalTo: tweetView.leftAnchor).isActive = true
            containerView.rightAnchor.constraint(equalTo: tweetView.rightAnchor).isActive = true
            containerView.isHidden = false
            postText.isHidden = false

        } else {
    // run only this code in background/async

            client.loadTweet(withID: tweetID, completion: { [weak self] (t, error) in
                if let weakSelf = self {
                    if let tweet = t {
                        weakSelf.twitterCache.addTweetToCache(tweet: tweet, forID: Int(tweetID)!)
                    }
                }
// completion on main

                if let complete = completion {
  DispatchQueue.main.async {
 complete()

    }   
                }
            })
        }

        if urlLessString.string.isEmpty {
            if let ogTitle = og[.title] {
                postText.text = String(htmlEncodedString: ogTitle)
            }
        } else {
            postText.attributedText = urlLessString
        }
    }

现在的顺序应该是这样的

  1. 第一次加载单元格
  2. 缓存中没有找到推文
  3. processForTwitter 中进行异步网络调用,但单元格返回时没有正确的数据
  4. 异步调用获取推文。并在主线程上调用完成,导致重新加载
  5. 再次在主线程上调用 configure
  6. 在主线程上找到了缓存中的推文。
  7. 添加你的 TWTRTweetView(仍然在主线程上),这将在返回单元格之前完成。

记住,在 cellForIndexPath 中返回单元格之前,应该在主线程上添加计算单元格大小的每个组件。


1
不确定这是否有效,但我相当确定在各个点上都是从主线程调用的。目前为止,昨天我终于找到了一个解决方法 - 显然,在estimatedRowHeight和reloadRows周围存在奇怪的行为,所以现在我缓存行高并在heightForRow中返回可用的行高...效果非常好! - Nick Coelius

0

我遇到了一个类似的问题。在

    [self.tableView beginUpdates];
    [self configureDataSource]; // initialization of cells by dequeuing 
    [self.tableView insertSections:[[NSIndexSet alloc] initWithIndex:index] withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView endUpdates];

还有之后

    [self.tableView beginUpdates];
    [self configureDataSource]; // initialization of cells by dequeuing  
    [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:index] withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView endUpdates];

我的TableViewController有两行重叠了。

然后我找到了问题所在。

在beginUpdate和endUpdate之后,还有两个方法被调用:estimatedHeightForRowAtIndexPath和heightForRowAtIndexPath。但是没有调用cellForRowAtIndexPath。

其中一个重叠的单元格已经被出列,但是我只在cellForRowAtIndexPath方法中设置了它的文本/值。因此,单元格高度计算正确,但内容不正确。

我的解决方法是在出列单元格后也设置文本/值。因此,唯一的区别在于我的configureDataSource方法内部:

    [self.tableView beginUpdates];
    [self configureDataSource]; // init of cells by dequeuing and set its texts/values  
    // delete or insert section
    [self.tableView endUpdates];

(其他可能的解决方法是避免单元格的重新初始化。但这种缓存已初始化单元格的方法对于具有大量单元格的TableViews来说是不好的)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接