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

Swift Firestore TableView自定义Cell'eat'按钮匹配排序异常Bug

问题效果演示

问题描述

搭载自定义Cell的TableView已实现Firestore实时数据监听,拉取到的菜单数据按date字段降序排序后存入本地menu数组,但点击eat按钮操作餐食数据时,按钮状态无法和正确排序后的列表项匹配对应。

现有核心代码

loadMenu() 函数

func loadMenu() {
    
    let menuRef = db.collection("menu").document()
    
    db.collection("menu").order(by: "date", descending: true).whereField("family_id", isEqualTo: "\(UserDefaults.standard.string(forKey: "family_id")!)")
        .addSnapshotListener { querySnapshot, error in
            self.menu = []
            guard let documents = querySnapshot?.documents else {
                print("Error fetching documents: \(error!)")
                return
            }
            
            let name = documents.map { $0["name"] ?? [""] }
            let family_id = documents.map { $0["family_id"] ?? [0] }
            let portions = documents.map { $0["portions"] ?? [""] }
            let menu_id = documents.map { $0["menu_id"] ?? [""]}
            let isOpened = documents.map { $0["isOpened"] ?? [""]}
            
            if name != nil || name[0] as! String != "" {
                for i in 0..<name.count {
                    self.menu.append(Menu(menu_id: menu_id[i] as! String, name: name[i] as! String, family_id: family_id[i] as! String, portions: portions[i] as! Int, isOpened: isOpened[i] as! Bool))
                }
            }
            
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
}

eatButtonPressed() 函数

func eatButtonPressed(cell: TopPartTableViewCell, send: UIButton) {
    var likeRef = self.db.collection("like").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)")
    
    self.db.collection("dislike").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)").delete() { err in
        if let err = err {
            print("Error removing document: \(err)")
        } else {
            print("Document successfully removed!")
        }
    }

    likeRef.getDocument { (document, error) in
        if let document = document, document.exists {
            let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
            self.db.collection("like").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)").delete() { err in
                if let err = err {
                    print("Error removing document: \(err)")
                } else {
                    print("Document successfully removed!")
                }
            }
        } else {
            likeRef.setData([
                "like_id": "\(likeRef.documentID)",
                "user_id": "\(Auth.auth().currentUser!.uid)",
                "menu_id": "\(self.menu[send.tag].menu_id)"
            ]) { err in
                if let err = err {
                    print("Error writing document: \(err)")
                } else {
                    print("Document successfully written!")
                }
            }
        }
    }
    
    let user_liked = like.contains(where: {$0.menu_id == menu[send.tag].menu_id}) && like.contains(where: {$0.user_id == Auth.auth().currentUser!.uid})
    
    if !user_liked {
        send.backgroundColor = UIColor(named: "BrandOrange")
        send.tintColor = UIColor.white
        
        cell.dontEatButton.backgroundColor = UIColor.white
        cell.dontEatButton.tintColor = UIColor.black
    } else {
        send.backgroundColor = UIColor.white
        send.tintColor = UIColor.black
        
    }
}
问题根因
  • 索引用法不可靠:通过send.tag绑定数组索引的方案存在天然缺陷,列表排序更新、Cell复用场景下,tag值不会自动同步最新索引位置,直接导致取到的menu数组元素错位。
  • 状态更新时机错误:Firestore读写属于异步操作,判断user_liked状态的代码写在异步回调外层,执行时接口数据还未返回、本地like数组未同步,拿到的是旧状态。
  • 状态管理不统一:仅在点击按钮时修改按钮样式,tableView(_:cellForRowAt:)数据源方法中没有根据真实数据重置按钮状态,Cell复用时会残留旧样式。
  • 类型解析存在风险:字段解析时默认值类型和实际字段类型不匹配,比如Int类型的portions、Bool类型的isOpened都用数组作为默认值,强制类型转换时会触发崩溃,也会导致数据错位。
修复方案

1. 废弃tag绑定索引的方案

点击按钮时不依赖tag取值,通过按钮视图层级定位到所属Cell,再从TableView获取Cell对应的真实IndexPath,从根源避免索引错位。

2. 统一在数据源方法中配置Cell状态

每次Cell渲染时,都根据当前menu项和like/dislike数组的真实数据重置两个按钮的样式,避免Cell复用导致的状态错乱。

3. UI更新放到异步操作完成后执行

所有Firestore读写操作完成、本地数据同步后,再刷新对应行的Cell,走统一的状态配置逻辑,不要在异步逻辑外提前更新UI。

4. 修复字段类型解析逻辑

给每个字段设置对应类型的默认值,过滤无效空数据,避免强制类型转换崩溃。

修复后的核心代码示例

1. cellForRowAt 状态统一配置

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TopPartTableViewCell", for: indexPath) as! TopPartTableViewCell
    let currentMenu = menu[indexPath.row]
    // 绑定按钮事件,无需设置tag
    cell.eatButton.addTarget(self, action: #selector(eatButtonPressed(sender:)), for: .touchUpInside)
    
    let currentUid = Auth.auth().currentUser!.uid
    // 按真实数据重置按钮状态
    let isLiked = like.contains { $0.menu_id == currentMenu.menu_id && $0.user_id == currentUid }
    let isDisliked = dislike.contains { $0.menu_id == currentMenu.menu_id && $0.user_id == currentUid }
    
    if isLiked {
        cell.eatButton.backgroundColor = UIColor(named: "BrandOrange")
        cell.eatButton.tintColor = .white
        cell.dontEatButton.backgroundColor = .white
        cell.dontEatButton.tintColor = .black
    } else if isDisliked {
        cell.dontEatButton.backgroundColor = UIColor(named: "BrandOrange")
        cell.dontEatButton.tintColor = .white
        cell.eatButton.backgroundColor = .white
        cell.eatButton.tintColor = .black
    } else {
        cell.eatButton.backgroundColor = .white
        cell.eatButton.tintColor = .black
        cell.dontEatButton.backgroundColor = .white
        cell.dontEatButton.tintColor = .black
    }
    // 其余Cell配置逻辑
    return cell
}

2. 修复后的eatButtonPressed逻辑

@objc func eatButtonPressed(sender: UIButton) {
    // 获取真实的Cell和IndexPath
    guard let cell = sender.superview?.superview as? TopPartTableViewCell,
          let indexPath = tableView.indexPath(for: cell) else { return }
    let currentMenu = menu[indexPath.row]
    let currentUid = Auth.auth().currentUser!.uid
    let docId = "\(currentUid)_\(currentMenu.menu_id)"
    let likeRef = db.collection("like").document(docId)
    let dislikeRef = db.collection("dislike").document(docId)
    let group = DispatchGroup()
    
    // 删除对应dislike记录
    group.enter()
    dislikeRef.delete { _ in group.leave() }
    
    // 处理like记录
    group.enter()
    likeRef.getDocument { doc, _ in
        defer { group.leave() }
        if doc?.exists == true {
            likeRef.delete()
        } else {
            likeRef.setData([
                "like_id": docId,
                "user_id": currentUid,
                "menu_id": currentMenu.menu_id
            ])
        }
    }
    
    // 所有操作完成后刷新对应行
    group.notify(queue: .main) {
        self.tableView.reloadRows(at: [indexPath], with: .none)
    }
}

3. 修复后的loadMenu字段解析逻辑

// 替换原有字段解析代码,使用类型匹配的默认值
let names = documents.map { $0["name"] as? String ?? "" }
let familyIds = documents.map { $0["family_id"] as? String ?? "" }
let portions = documents.map { $0["portions"] as? Int ?? 0 }
let menuIds = documents.map { $0["menu_id"] as? String ?? "" }
let isOpeneds = documents.map { $0["isOpened"] as? Bool ?? false }

self.menu.removeAll()
for i in 0..<documents.count {
    // 过滤无效空数据
    guard !names[i].isEmpty, !menuIds[i].isEmpty else { continue }
    self.menu.append(Menu(
        menu_id: menuIds[i],
        name: names[i],
        family_id: familyIds[i],
        portions: portions[i],
        isOpened: isOpeneds[i]
    ))
}

内容的提问来源于stack exchange,提问作者Bill Rei

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 03:09:39