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

Swift TableView删除行异常:Firestore数据排序与索引不匹配

Twitter克隆应用删除帖子索引不匹配问题解决

问题背景

开发Twitter克隆应用,已完成Firestore对接。从Firestore读取帖子时,通过timestamp字段排序获取有序数组,为让最新帖子显示在顶部,将新帖子插入数组索引0位置,导致数组索引与显示顺序反向(最底部的帖子对应数组索引0)。删除最新帖子时,实际删除的是最底部的帖子——虽然Firestore中已正确删除,刷新后显示恢复正常,但视觉层面存在错误,需要找到不依赖索引删除选中单元格的方案。

相关代码

Firestore数据监听代码

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){
            self.posts = tempPosts
            tempPosts.insert(post, at: 0)
        }
    }
    self.posts.removeAll()
    self.posts = tempPosts
    self.tableView.reloadData()
}

TableView单元格配置与删除代码

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
        let currentIndexPath = tableView.indexPath(for: aCell)!
        self.posts.remove(at: currentIndexPath.row)
        print(currentIndexPath.row) // 删除最新帖子时打印0,但实际删除的是最底部帖子
        tableView.deleteRows(at: [currentIndexPath], with: .automatic)
    }
    
    return cell
}

单元格类代码

import UIKit
import FirebaseAuth
import FirebaseFirestore

class PostTableViewCell: UITableViewCell, UITextViewDelegate{

    @IBOutlet weak var profileImg: UIImageView!
    @IBOutlet public weak var username: UILabel!
    @IBOutlet weak var timestamp: UILabel!

    @IBOutlet weak var postText: UITextView!

    @IBOutlet weak var menuButton: UIButton!

    public var postId: String!

    var didDelete : ((UITableViewCell) -> Void)?

    override func awakeFromNib() {
        super.awakeFromNib()
        
        profileImg.layer.cornerRadius = profileImg.bounds.height / 2
        profileImg.clipsToBounds = true
        
        postText.delegate = self
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

    func set(post: Post){
        username.text = post.username
        postText.text = post.text
        timestamp.text = post.createdAt.calenderTimeSinceNow()
        postId = post.id
        
        setPopupMenu(post: post)
    }

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if(text == "\n") {
            postText.resignFirstResponder()
            postText.isEditable = false
            postText.isScrollEnabled = false
            return false
        }
        return true
    }

    func setPopupMenu(post: Post){
        let options = { (action: UIAction) in
            print(action.title)
            
            if(action.title == "تعديل"){
                self.postText.isEditable = true
                self.postText.isScrollEnabled = true
                self.postText.becomeFirstResponder()
            }
            
            else if(action.title == "حذف"){
                let db = Firestore.firestore()
                db.collection("Posts").document(Auth.auth().currentUser!.uid).collection("Posts").document(post.id).delete() { err in
                    if let err = err {
                        print("Error removing document: \(err)")
                    } else {
                        self.didDelete!(self)
                    }
                }
            }
        }
        
        if(post.userId == Auth.auth().currentUser!.uid){
            self.menuButton.menu = UIMenu(children: [
                UIAction(title: "تعديل", image: UIImage(systemName: "pencil"), handler: options),
                UIAction(title: "حذف", image: UIImage(systemName: "trash"), attributes: .destructive, handler: options),
            ])
        }
        else{
            self.menuButton.menu = UIMenu(children: [
                UIAction(title: "حظر", image: UIImage(systemName: "pencil"), handler: options),
                UIAction(title: "ميوت", image: UIImage(systemName: "trash"), attributes: .destructive, handler: options),
            ])
        }
        
        menuButton.showsMenuAsPrimaryAction = true
    }
}

解决方案

核心思路:通过帖子唯一ID定位删除,摆脱索引依赖

直接利用每个帖子的id(Firestore文档ID)作为唯一标识,在删除时通过ID找到数组中的对应元素,而非依赖单元格的indexPath。同时可以优化数据加载逻辑,从根源避免索引与显示顺序反向的问题。

具体修改步骤

1. 调整单元格的删除回调定义

将PostTableViewCell中的didDelete闭包改为传递帖子ID,而非单元格本身:

// 替换原有的didDelete定义
var didDelete : ((String) -> Void)?

2. 修改单元格中删除成功后的回调调用

在删除Firestore文档成功后,调用闭包时传入当前帖子的ID:

else if(action.title == "حذف"){
    let db = Firestore.firestore()
    db.collection("Posts").document(Auth.auth().currentUser!.uid).collection("Posts").document(post.id).delete() { err in
        if let err = err {
            print("Error removing document: \(err)")
        } else {
            // 传入post.id而非self
            self.didDelete?(post.id)
        }
    }
}

3. 更新TableView中的删除处理逻辑

收到帖子ID后,在数组中找到匹配的元素并删除,再更新TableView:

cell.didDelete = { postId in
    // 通过postId找到数组中的对应索引
    if let index = self.posts.firstIndex(where: { $0.id == postId }) {
        self.posts.remove(at: index)
        tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
    }
}

额外优化:修正Firestore数据加载逻辑

原代码中手动插入索引0的方式容易出错,可让Firestore直接返回降序排列的结果,无需手动调整顺序:

db.collection("Posts").document(doc.documentID).collection("Posts")
    .order(by: "timestamp", descending: true) // 添加descending参数,直接获取降序结果
    .addSnapshotListener { documentSnapshot, error in
        guard let documents = documentSnapshot?.documents else {
            print("Error fetching documents: \(error!)")
            return
        }
        
        var newPosts = [Post]()
        for docu in 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)
            newPosts.append(post)
        }
        // 直接赋值,最新帖子已在数组首位
        self.posts = newPosts
        self.tableView.reloadData()
}

内容的提问来源于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 06:18:29