Custom UITableview Cell Occasionally Overlapping/Reproducing On Top Of Other Cell

早过忘川 提交于 2019-12-23 17:58:30

问题


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

  1. Cell is loaded for first time
  2. No tweet found in cache
  3. Async network call in processForTwitter and cell is returned without proper data
  4. Async call gets tweet. And calls completion on main thread which results in reload
  5. configure for is called again on main thread
  6. tweet is found in cache while remaining on main thread.
  7. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!