You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift UITableView删除单元格后重复出现的问题求助

问题:Firebase删除帖子后TableView数据异常,已删除帖子复现并触发数组越界

我正在开发一个Twitter克隆应用,已接入Firebase实现帖子的读写操作。但删除TableView单元格时出现以下异常:

  • 首次删除帖子时动画正常执行,posts数组也移除了对应数据
  • 删除另一个帖子时,之前已删除的帖子会重新出现
  • 最终删除所有单元格时会触发数组越界异常

异常流程说明:

  1. 首次删除帖子,界面和数据表现正常
  2. 帖子成功从界面消失
  3. 尝试删除另一帖子
  4. 之前已删除的帖子在刚删除的位置重新出现

相关代码

帖子单元格代码(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文档删除代码后,一切正常,已删除的帖子不会复现。


解决方案

问题根源

  1. 重复注册Snapshot Listener:readPosts方法中,每次外层集合snapshot触发时,都会给每个用户的子集合新增监听器,重复的监听器会不断拉取数据并刷新界面,导致已删除帖子复现。
  2. 删除时索引失效:删除回调中使用cellForRowAt时的indexPath.row,若在此期间数据源被监听器更新,该索引会失效,导致删除错误元素,引发数组越界。
  3. 数据同步逻辑冲突:手动修改本地数组+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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 07:03:30