UITableView滚动时自定义Cell中UIButton标题状态丢失问题
Hey there! Let's fix that button state loss issue when your UITableView scrolls. The root problem here is cell reuse: when cells scroll off-screen, they get recycled for new rows, and your current code resets the button to its default state every time cellForRowAt runs—completely ignoring the selected state you set earlier. Here's how to fix it step by step:
1. Add a selection state property to your data model
First, you need to track the selected state for each user in your data source (not just the button UI). If your user model is named something like User, add a boolean property to it:
class User { // Your existing properties: firstname, lastname, jobTitle, avatar... var isSelected: Bool = false // Add this to track selection state }
2. Update cellForRowAt to use the model's state
Instead of hardcoding the button's default state every time, configure it based on the isSelected value from your user model. Replace your current button setup code in cellForRowAt with this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "FrendsTableViewCellId", for: indexPath) as! FriendEventialTableViewCell guard let f = self.user?[indexPath.row] else { return cell } // Configure basic cell content (unchanged) cell.userName.text = "\(f.firstname!) \(f.lastname!)" cell.titleName.text = f.jobTitle ?? "" if let avatarUrl = f.avatar { cell.imageUser.af_setImage(withURL: URL(string: avatarUrl)!) } // Configure button based on model's selection state if f.isSelected { cell.selectButton.setTitle("Selected", for: .normal) cell.selectButton.setTitleColor(ColorConstant.charcoalGrey, for: .normal) cell.selectButton.layer.borderColor = UIColor(red: 215.0/255.0, green: 190.0/255.0, blue: 10.0/255.0, alpha: 1.0).cgColor cell.selectButton.backgroundColor = ColorConstant.sunYellow cell.selectButton.isSelected = true } else { cell.selectButton.setTitle("Select", for: .normal) cell.selectButton.setTitleColor(ColorConstant.charcoalGrey, for: .normal) cell.selectButton.backgroundColor = .white cell.selectButton.layer.borderColor = UIColor(red: 151.0/255.0, green: 151.0/255.0, blue: 151.0/255.0, alpha: 1.0).cgColor cell.selectButton.isSelected = false } // Optional: Move button target setup to the cell class (see step 4) cell.selectButton.addTarget(self, action: #selector(followButtonClicked(sender:)), for: .touchUpInside) cell.selectButton.tag = indexPath.row return cell }
3. Modify the button tap handler to update the model
Instead of only changing the button's UI, update the corresponding user model's isSelected state, then refresh the cell to reflect the change:
@objc func followButtonClicked(sender:UIButton) { guard let indexPath = tableView.indexPathForRow(at: sender.convert(.zero, to: tableView)) else { return } guard var user = self.user?[indexPath.row] else { return } // Toggle the selection state in the model user.isSelected = !user.isSelected self.user![indexPath.row] = user // Refresh the cell to apply the new state (avoids reuse issues) tableView.reloadRows(at: [indexPath], with: .automatic) // Optional: If you don't want to reload the row, you can manually update the button UI here // But reloading is more reliable for avoiding reuse bugs }
4. Bonus: Clean up button target setup (optional but recommended)
Adding the button target in cellForRowAt can lead to multiple target attachments as cells are reused. Instead, move this setup to your custom cell class and use a closure to pass the tap event back to the view controller:
In FriendEventialTableViewCell:
class FriendEventialTableViewCell: UITableViewCell { // Your existing @IBOutlet connections @IBOutlet weak var userName: UILabel! @IBOutlet weak var titleName: UILabel! @IBOutlet weak var imageUser: UIImageView! @IBOutlet weak var selectButton: UIButton! // Closure to handle button taps var onButtonTap: (() -> Void)? override func awakeFromNib() { super.awakeFromNib() // Add target once when the cell is initialized selectButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) } @objc private func buttonTapped() { onButtonTap?() } }
Then update cellForRowAt to use the closure:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "FrendsTableViewCellId", for: indexPath) as! FriendEventialTableViewCell guard let f = self.user?[indexPath.row] else { return cell } // ... (keep your existing cell content configuration) // Set up the button tap closure cell.onButtonTap = { [weak self] in guard let self = self else { return } var updatedUser = f updatedUser.isSelected = !updatedUser.isSelected self.user![indexPath.row] = updatedUser self.tableView.reloadRows(at: [indexPath], with: .automatic) } return cell }
The key takeaway here is: always store state in your data model, not just in UI controls. Since UITableView reuses cells, the UI will reset when cells scroll, but your model's state will persist—ensuring the cell always shows the correct state when it's reused.
内容的提问来源于stack exchange,提问作者Irwan Madness

