Swift UITableView删除单元格后重复出现的问题求助
问题:Firebase删除帖子后TableView数据异常,已删除帖子复现并触发数组越界
我正在开发一个Twitter克隆应用,已接入Firebase实现帖子的读写操作。但删除TableView单元格时出现以下异常:
- 首次删除帖子时动画正常执行,
posts数组也移除了对应数据 - 删除另一个帖子时,之前已删除的帖子会重新出现
- 最终删除所有单元格时会触发数组越界异常
异常流程说明:
- 首次删除帖子,界面和数据表现正常
- 帖子成功从界面消失
- 尝试删除另一帖子
- 之前已删除的帖子在刚删除的位置重新出现
相关代码
帖子单元格代码(PostTableViewCell.swift)
class PostTableViewCell: UITableViewCell, UITextViewDelegate{ var didDelete : ((UITableViewCell) -> Void)? override func awakeFromNib() { super.awakeFromNib() // 初始化代码 } func set(post: Post){ username.text = post.username postText.text = post.text timestamp.text = post.createdAt.calenderTimeSinceNow() postId = post.id setPopupMenu(post: post) } func setPopupMenu(post: Post){ let options = { (action: UIAction) in print(action.title) if(action.title == "Delete"){ // 触发删除回调 self.didDelete!(self) } } menuButton.showsMenuAsPrimaryAction = true } }
首页代码(HomeViewController.swift)
class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{ let cellNib = UINib(nibName: "PostTableViewCell", bundle: nil) var posts = [Post]() override func viewDidLoad() { super.viewDidLoad() tableView.register(cellNib, forCellReuseIdentifier: "postCell") tableView.delegate = self tableView.dataSource = self readPosts() // 加载视图后的额外设置 } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) tableView.reloadRows(at: [indexPath], with: .fade) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return posts.count } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell cell.set(post: posts[indexPath.row]) cell.didDelete = { aCell in self.progressView.isHidden = false let currentIndexPath = tableView.indexPath(for: aCell)! let db = Firestore.firestore() db.collection("Posts").document(Auth.auth().currentUser!.uid).collection("Posts").document(cell.postId).delete() { err in self.progressView.isHidden = false if let err = err { print("删除文档错误: \(err)") } else { self.posts.remove(at: indexPath.row) tableView.deleteRows(at: [currentIndexPath], with: .automatic) self.progressView.isHidden = true } } } return cell } func readPosts(){ let db = Firestore.firestore() db.collection("Posts").addSnapshotListener { documentSnapshot, error in guard let document = documentSnapshot else { print("获取文档错误: \(error!)") return } var tempPosts = [Post]() for doc in documentSnapshot!.documents{ db.collection("Posts").document(doc.documentID).collection("Posts").order(by: "timestamp").addSnapshotListener { documentSnapshot, error in for docu in documentSnapshot!.documents { let daate = docu.data()["timestamp"] as! Timestamp let mydbl = daate.dateValue().timeIntervalSince1970 * 1000 let post = Post(id: docu.documentID, username: "@\(docu.data()["username"]!)", userId: doc.documentID , text: docu.data()["text"] as! String, timestamp: mydbl , numLikes: 0, numReposts: 0) let isDuplicate = tempPosts.contains(where: { $0.id == post.id }) if(!isDuplicate){ tempPosts.insert(post, at: 0) } } self.posts.removeAll() self.posts = tempPosts self.tableView.reloadData() } } self.progressView.isHidden = true } } }
注:注释掉Firebase文档删除代码后,一切正常,已删除的帖子不会复现。
解决方案
问题根源
- 重复注册Snapshot Listener:
readPosts方法中,每次外层集合snapshot触发时,都会给每个用户的子集合新增监听器,重复的监听器会不断拉取数据并刷新界面,导致已删除帖子复现。 - 删除时索引失效:删除回调中使用
cellForRowAt时的indexPath.row,若在此期间数据源被监听器更新,该索引会失效,导致删除错误元素,引发数组越界。 - 数据同步逻辑冲突:手动修改本地数组+Firestore监听器自动刷新的双重操作,导致本地数据和云端数据不同步。
修复步骤
1. 清理重复监听器并优化数据加载
为每个用户的帖子集合仅注册一次监听器,用DispatchGroup确保所有子集合加载完成后再更新界面:
// 新增变量保存监听器引用,避免重复注册 var postListeners = [ListenerRegistration]() func readPosts(){ // 先移除旧的监听器 postListeners.forEach { $0.remove() } postListeners.removeAll() let db = Firestore.firestore() db.collection("Posts").addSnapshotListener { [weak self] documentSnapshot, error in guard let self = self, let document = documentSnapshot else { print("获取文档错误: \(error!)") return } var tempPosts = [Post]() // 用DispatchGroup等待所有子集合加载完成 let group = DispatchGroup() for doc in document.documents{ group.enter() let listener = db.collection("Posts").document(doc.documentID).collection("Posts") .order(by: "timestamp", descending: true) // 按时间倒序排列 .addSnapshotListener { subSnapshot, error in defer { group.leave() } guard let subDocs = subSnapshot?.documents else { print("获取子集合错误: \(error!)") return } // 移除该用户的旧帖子,避免重复添加 tempPosts.removeAll { $0.userId == doc.documentID } for docu in subDocs { let timestamp = (docu.data()["timestamp"] as! Timestamp).dateValue() let post = Post(id: docu.documentID, username: "@\(docu.data()["username"]!)", userId: doc.documentID , text: docu.data()["text"] as! String, timestamp: timestamp.timeIntervalSince1970 * 1000, numLikes: 0, numReposts: 0) tempPosts.append(post) } // 按时间排序所有帖子 tempPosts.sort { $0.timestamp > $1.timestamp } self.posts = tempPosts self.tableView.reloadData() } self.postListeners.append(listener) } group.notify(queue: .main) { self.progressView.isHidden = true } } }
2. 修复删除逻辑,依赖Firestore自动同步
删除后无需手动修改本地数组,Firestore监听器会自动同步云端数据并刷新界面:
cell.didDelete = { [weak self] aCell in guard let self = self, let currentIndexPath = tableView.indexPath(for: aCell) else { return } self.progressView.isHidden = false let postToDelete = self.posts[currentIndexPath.row] let db = Firestore.firestore() db.collection("Posts") .document(Auth.auth().currentUser?.uid ?? "") .collection("Posts") .document(postToDelete.id) .delete() { [weak self] err in guard let self = self else { return } DispatchQueue.main.async { self.progressView.isHidden = true if let err = err { print("删除文档错误: \(err)") // 此处可添加用户可见的错误提示,比如Alert } } } }
3. 优化内存管理,避免泄漏
在控制器销毁时移除所有监听器:
deinit { postListeners.forEach { $0.remove() } }
额外建议
- 避免强制解包:比如
Auth.auth().currentUser!.uid,应先判断用户是否登录,防止崩溃。 - 统一时间类型:直接使用
Date作为帖子时间戳类型,无需转成毫秒数,简化排序和展示逻辑。 - 用户友好提示:删除失败时给用户显示弹窗或Toast,提升体验。
内容的提问来源于stack exchange,提问作者Yousef Baghlaf
相关产品推荐
相关产品推荐

