swiftuitableviewnotificationscompletionhandler

How to make sure a line of code is executed after another is completed?


I have a viewControl called PostViewController which has a UITableView of posts. I also have a class called PostCell which defines the UITableViewCell. I made a button function in PostCell called likeButtonClicked to favour a post similar to twitter.

@IBAction func likesButtonClicked(_ sender: Any) { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil, userInfo: ["cell":self, "likesButton":likesButton!]) }

This is to pass the cell indexPath and the button name to PostViewController. I need indexPath to increase the likes by 1 and the button name to change its image to pink when post is favoured.

I then subscribed to the notification in viewDidLoad of PostViewController.

 NotificationCenter.default.addObserver(self, selector: #selector(postLiked), name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil)

I then wrote this function in the same viewController

  @objc func postLiked(notification: Notification){
    if let cell = notification.userInfo?["cell"] as? UITableViewCell{
        let likesButton = notification.userInfo?["likesButton"] as? SpringButton
        if let indexPath = postsTableView.indexPath(for: cell){
            let post = posts[indexPath.row]
            postId = post.id
            PostAPI.getPostById(postId: postId) { post in
                //Check if the same post were already favoured.
                if !self.liked || self.oldPostId != self.postId{
                    self.newLikes = post.likes + 1
                    self.liked = true
                    self.oldPostId = self.postId
                }else{
                    self.newLikes = self.newLikes - 1
                    self.liked = false
                }
                PostAPI.favourPost(postId: self.postId, likes: self.newLikes) {
                    PostAPI.getPostById(postId: self.postId) { postResponse in
                        let post = postResponse
                        self.posts[indexPath.row] = post
                        let cellNumber = IndexPath(row: indexPath.row, section: indexPath.section)
                        self.reloadRowData(cellNumber: cellNumber){
                            if !self.liked{
                                likesButton?.tintColor = .systemPink
                            }else{
                                likesButton?.tintColor = .darkGray
                            }
                        }
                    }
                }
            }
        }
    }
}
func reloadRowData(cellNumber: IndexPath, completion: @escaping () -> ()) {
    self.postsTableView.reloadRows(at: [cellNumber], with: .none)
    completion()
}

Please tell me why the last 4 lines of postLiked function is executed before reloadRowData function, which causes the button to change its color to pink then returns immediately to gray when it should stay pink.

Any help will be most appreciated. Thank you.


Solution

  • I expect the specific problem is your call to reloadRows. As the docs note:

    Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.

    So this is likely creating an entirely new cell, and then the later code modifies the old cell that is being removed from the table. (So you see the change, and then the cell is removed and replaced.)

    I would start by getting rid of the entire reloadRowData call.

    Generally, though, this code is fragile, and I'd redesign it. The cell should take care of setting the tint colors itself based on the data. You generally shouldn't be reaching into a cell and manipulating its subviews. This will cause you a problem when cells are recycled (for example, when this scrolls off screen). All the configuration should be done in cellForRow(at:), and the cell should observe its Post and update itself when there are changes.

    Data should live in the Model. The View should observe the Model and react. The Model should not reach into the View and manipulate anything.

    As a side: your reloadRowData looks async, but it's not. There's no reason for a completion handler. It could just call return.