问题
Really strange problem I'm having here.
Basically, the user comes to a View that has a TableView attached to it. There's a call to the backend for some data, and in the completion block of that data is provided to the TableView to display.
In cellForRow 1 of 4 reusable, custom cells is dequeued, based on some logic, and then a configure method for that cell is called.
Here's where it gets tricky: Some of the cells might require further asynchronous calls (to fetch more data for their custom displays), thus required a tableView.reloadRows call...What's happening, then, is best explained by these two screenshots:
In the first one, that cell with "Ever wonder what YOU'D do for a klondike bar?" is replicated on top of the "Self-Management for Actor's" cell which, as a cell containing Twitter info DID require an additional asynch call to fetch that data.
Any thoughts? I'm running the tableView.reloadRows call inside of a begin/endUpdates block, but no dice.
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
}
asynch Twitter call:
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
}
}
completion block in the tableviewcontroller:
func reload(postID: Int) {
tableView.beginUpdates()
self.tableView.reloadRows(at: [self.postToIndexPath[postID]!], with: .automatic)
tableView.endUpdates()
}
NB: this doesn't happen 100% of the time, it is, presumably, network dependent.
回答1:
I guess the issue lies in calculating the size of the components that are present on cell. It's like cell is dequed and frame along with subviews are set before data is available.
Maybe you can try,
tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = yourPrefferedHeight
but you need to make sure that your autolayout constraints are proper and cell can determine it's height if all the components has got height.
回答2:
Turns out there's weird, or at least I don't understand it, behavior around estimatedRowHeight
and reloadRows
; solution, via a tangentially related SO question, was to cache the row heights as they become available and return that if available in heightForRow
.
I guess it doesn't recalculate the heights, or something, on a reload? Really don't understand specifically WHY this works, but I'm happy it does!
回答3:
I am assuming processForTwitter
is called on background/async from within configureFor
. If true this is the fault. Your api call should be on back ground but if you are getting tweet from your cache than it addSubview should be done on main thread and before returning cell. Try doing this, make sure that processForTwitter
is called on main thread and configure it as
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
}
}
Now sequence would be like this
- Cell is loaded for first time
- No tweet found in cache
- Async network call in
processForTwitter
and cell is returned without proper data - Async call gets tweet. And calls completion on main thread which results in reload
- configure for is called again on main thread
- tweet is found in cache while remaining on main thread.
- add your TWTRTweetView (you are still on main thread) and this will be done before your cell is returned.
remember this, every component in your cell which will play role in calculating size of cell should be added on main thread before returning cell in cellForIndexPath
回答4:
After the async call, instead of tableView.reloadRows
try to call:
tableView.beginUpdates()
tableView.setNeedsLayout()
tableView.endUpdates()
It seems like the data get populated, but the size of the cell does not get reflected. If you call reloadRows
I'm afraid it does not recalculate the new positions of the following cells - that means if cell row 3 resizes to a bigger height, it will get bigger, but the following cells won't get pushed further down.
回答5:
I came across a similar problem. After
[self.tableView beginUpdates];
[self configureDataSource]; // initialization of cells by dequeuing
[self.tableView insertSections:[[NSIndexSet alloc] initWithIndex:index] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
and also after
[self.tableView beginUpdates];
[self configureDataSource]; // initialization of cells by dequeuing
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:index] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
two rows of my TableViewController were overlapped.
Then I found out where's the catch.
After beginUpdate and endUpdate are two methods called for cells:estimatedHeightForRowAtIndexPath and heightForRowAtIndexPath. But there's no call of cellForRowAtIndexPath.
One of overlapping cells was dequeued but I set it's text/value only in cellForRowAtIndexPath method. So cell height was computed correctly but for incorrect content.
My fix was to set text/value also after dequeuing the cell. So the only difference is inside my configureDataSource method:
[self.tableView beginUpdates];
[self configureDataSource]; // init of cells by dequeuing and set its texts/values
// delete or insert section
[self.tableView endUpdates];
(Other possible fix is to avoid reinitialization of cells. But this caching of initialized cells wouldn't be good for TableViews with tons of cells)
来源:https://stackoverflow.com/questions/48650696/custom-uitableview-cell-occasionally-overlapping-reproducing-on-top-of-other-cel