自定义 UITableview 单元格偶尔 Overlapping/Reproducing 在其他单元格之上
Custom UITableview Cell Occasionally Overlapping/Reproducing On Top Of Other Cell
我遇到的问题真的很奇怪。
基本上,用户来到一个附加了 TableView 的视图。调用后端获取一些数据,并在该数据的完成块中提供给 TableView 进行显示。
在 cellForRow 1 of 4 reusable 中,自定义单元格根据某种逻辑出列,然后调用该单元格的配置方法。
这就是它变得棘手的地方:一些单元格可能需要进一步的异步调用(为它们的自定义显示获取更多数据),因此需要一个 tableView.reloadRows 调用......那么发生的事情是最好的由这两个屏幕截图解释:
在第一个中,带有 "Ever wonder what YOU'D do for a klondike bar?" 的单元格被复制到 "Self-Management for Actor's" 单元格的顶部,作为包含 Twitter 信息的单元格,DID 需要额外的异步调用来获取该数据。
有什么想法吗?我是 运行 tableView.reloadRows 在 begin/endUpdates 块内调用,但没有骰子。
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()
}
注意:这不会 100% 发生,大概是网络相关的。
异步调用后,尝试调用
而不是 tableView.reloadRows
tableView.beginUpdates()
tableView.setNeedsLayout()
tableView.endUpdates()
似乎填充了数据,但未反映单元格的大小。如果您调用 reloadRows
恐怕它不会重新计算以下单元格的新位置 - 这意味着如果单元格行 3 调整到更大的高度,它会变大,但以下单元格不会被推再往下。
我想问题出在计算单元格上存在的组件的大小上。这就像在数据可用之前设置单元格和子视图以及框架。
也许你可以试试,
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 你的首选身高
但是你需要确保你的自动布局约束是正确的,并且如果所有组件都有高度,单元格可以确定它的高度。
我假设 processForTwitter
是在 background/async 上从 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
}
}
现在顺序是这样的
- 第一次加载单元格
- 缓存中未找到推文
processForTwitter
中的异步网络调用和返回的单元格没有正确的数据
- 异步调用获取推文。并在主线程上调用完成,导致重新加载
- configure 在主线程上再次调用
- 在缓存中找到推文,同时保留在主线程中。
- 添加您的 TWTRTweetView(您仍在主线程中),这将在返回您的单元格之前完成。
记住这一点,在 cellForIndexPath
中返回单元格之前,单元格中将在计算单元格大小中起作用的每个组件都应该添加到主线程中
原来在 estimatedRowHeight
和 reloadRows
周围有奇怪的行为,或者至少我不明白;通过一个切线相关的 SO 问题,解决方案是在行高可用时缓存行高,并且 return 如果在 heightForRow
中可用。
我猜它不会在重新加载时重新计算高度或其他东西?真的不明白为什么这行得通,但我很高兴它行得通!
我遇到了类似的问题。
之后
[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 之后是为 cells:estimatedHeightForRowAtIndexPath 和 heightForRowAtIndexPath 调用的两个方法。但是没有调用 cellForRowAtIndexPath。
其中一个重叠单元格已出队,但我仅在 cellForRowAtIndexPath 方法中将其设置为 text/value。因此单元格高度计算正确但内容不正确。
我的解决方法是在单元格出队后也设置 text/value。所以唯一的区别是在我的 configureDataSource 方法中:
[self.tableView beginUpdates];
[self configureDataSource]; // init of cells by dequeuing and set its texts/values
// delete or insert section
[self.tableView endUpdates];
(其他可能的解决方法是避免重新初始化单元格。但是这种初始化单元格的缓存对于具有大量单元格的 TableView 来说并不好)
我遇到的问题真的很奇怪。
基本上,用户来到一个附加了 TableView 的视图。调用后端获取一些数据,并在该数据的完成块中提供给 TableView 进行显示。
在 cellForRow 1 of 4 reusable 中,自定义单元格根据某种逻辑出列,然后调用该单元格的配置方法。
这就是它变得棘手的地方:一些单元格可能需要进一步的异步调用(为它们的自定义显示获取更多数据),因此需要一个 tableView.reloadRows 调用......那么发生的事情是最好的由这两个屏幕截图解释:
在第一个中,带有 "Ever wonder what YOU'D do for a klondike bar?" 的单元格被复制到 "Self-Management for Actor's" 单元格的顶部,作为包含 Twitter 信息的单元格,DID 需要额外的异步调用来获取该数据。
有什么想法吗?我是 运行 tableView.reloadRows 在 begin/endUpdates 块内调用,但没有骰子。
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()
}
注意:这不会 100% 发生,大概是网络相关的。
异步调用后,尝试调用
而不是tableView.reloadRows
tableView.beginUpdates()
tableView.setNeedsLayout()
tableView.endUpdates()
似乎填充了数据,但未反映单元格的大小。如果您调用 reloadRows
恐怕它不会重新计算以下单元格的新位置 - 这意味着如果单元格行 3 调整到更大的高度,它会变大,但以下单元格不会被推再往下。
我想问题出在计算单元格上存在的组件的大小上。这就像在数据可用之前设置单元格和子视图以及框架。
也许你可以试试,
tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 你的首选身高
但是你需要确保你的自动布局约束是正确的,并且如果所有组件都有高度,单元格可以确定它的高度。
我假设 processForTwitter
是在 background/async 上从 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
}
}
现在顺序是这样的
- 第一次加载单元格
- 缓存中未找到推文
processForTwitter
中的异步网络调用和返回的单元格没有正确的数据- 异步调用获取推文。并在主线程上调用完成,导致重新加载
- configure 在主线程上再次调用
- 在缓存中找到推文,同时保留在主线程中。
- 添加您的 TWTRTweetView(您仍在主线程中),这将在返回您的单元格之前完成。
记住这一点,在 cellForIndexPath
中返回单元格之前,单元格中将在计算单元格大小中起作用的每个组件都应该添加到主线程中原来在 estimatedRowHeight
和 reloadRows
周围有奇怪的行为,或者至少我不明白;通过一个切线相关的 SO 问题,解决方案是在行高可用时缓存行高,并且 return 如果在 heightForRow
中可用。
我猜它不会在重新加载时重新计算高度或其他东西?真的不明白为什么这行得通,但我很高兴它行得通!
我遇到了类似的问题。
之后 [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 之后是为 cells:estimatedHeightForRowAtIndexPath 和 heightForRowAtIndexPath 调用的两个方法。但是没有调用 cellForRowAtIndexPath。
其中一个重叠单元格已出队,但我仅在 cellForRowAtIndexPath 方法中将其设置为 text/value。因此单元格高度计算正确但内容不正确。
我的解决方法是在单元格出队后也设置 text/value。所以唯一的区别是在我的 configureDataSource 方法中:
[self.tableView beginUpdates];
[self configureDataSource]; // init of cells by dequeuing and set its texts/values
// delete or insert section
[self.tableView endUpdates];
(其他可能的解决方法是避免重新初始化单元格。但是这种初始化单元格的缓存对于具有大量单元格的 TableView 来说并不好)