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
相关产品推荐
相关产品推荐

