Swift中UITableView的cellForRowAt索引重复异常问题求助
从Google Firestore逐个获取UserProfile用户数据,每获取一条就通过完成处理程序把用户添加到全局数组self.activeUsers,然后调用self.activeUsersTableView.reloadData()刷新列表。现在出现用户列表重叠的情况,已排除UI和自动布局问题——通过在cellForRowAt打印indexPath.row发现同一索引被重复打印,且reloadData()仅在完成处理程序这一处调用。
TableView初始化
private lazy var activeUsersTableView: UITableView = { let table = UITableView() table.register(UITableViewCell.self, forCellReuseIdentifier: "cell") table.dataSource = self table.delegate = self table.allowsSelection = false return table }()
视图加载与布局
override func viewDidLoad() { super.viewDidLoad() loadActiveUsersTableView() fetchActiveUsers() } func loadActiveUsersTableView() { self.view.addSubview(activeUsersTableView) self.activeUsersTableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate( [ self.activeUsersTableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 150), self.activeUsersTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 30), self.activeUsersTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -280), self.activeUsersTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -250), ] ) }
用户数据获取逻辑
func fetchActiveUsers() { let myCompletionHandler: (UserProfile) -> Void = { theUser in self.activeUsers.append(theUser) self.activeUsersTableView.reloadData() } self.chatsDocRef!.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let data = document.data() self.activeUserUIDS = data?["users"] as? [String] for i in 0..<(self.activeUserUIDS?.count ?? 0) { print("Retrieving document for user ID: ", self.activeUserUIDS![i]) let userDocRef = Firestore.firestore().collection("users").document(self.activeUserUIDS![i]) UserProfile.getUserProfileData(userDocRef, using: myCompletionHandler) } } } } }
Cell配置与打印输出
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { print("The row", indexPath.row) let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) return cell }
打印输出:
The row 0 The row 0 The row 1 The row 0 The row 1 The row 2 The row 0 The row 1 The row 2 The row 3 The row 0 The row 1 The row 2 The row 3 The row 4 The row 0 The row 1 The row 2 The row 3 The row 4 The row 5 The row 0 The row 1 The row 2 The row 3 The row 4 The row 5 The row 0 The row 1 The row 2 The row 3 The row 4 The row 5 The row 0 The row 1 The row 2 The row 3 The row 4 The row 5
核心问题在于Firestore的getDocument是异步操作,循环中逐个调用getUserProfileData时,这些请求会并行执行,完成顺序无法保证。每次有一条用户数据返回就调用reloadData(),而reloadData()会触发tableView重新加载所有可见cell,因此每新增一个用户,之前的所有行都会被重新调用cellForRowAt,导致同一索引重复打印。同时,异步请求的无序性可能导致activeUsers数组内的用户顺序与activeUserUIDS不一致,加上全量刷新时的cell复用问题,最终出现列表重叠的视觉异常。
方案1:所有请求完成后统一刷新
用DispatchGroup等待所有用户数据请求完成,再一次性将数据加入数组并刷新tableView,避免多次全量刷新:
func fetchActiveUsers() { guard let chatsDocRef = self.chatsDocRef else { return } chatsDocRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") return } guard let document = document, let data = document.data(), let activeUserUIDS = data["users"] as? [String] else { return } self.activeUserUIDS = activeUserUIDS let dispatchGroup = DispatchGroup() var fetchedUsers = [UserProfile]() for uid in activeUserUIDS { dispatchGroup.enter() let userDocRef = Firestore.firestore().collection("users").document(uid) UserProfile.getUserProfileData(userDocRef) { user in fetchedUsers.append(user) dispatchGroup.leave() } } dispatchGroup.notify(queue: .main) { // 若需保持与activeUserUIDS一致的顺序,建议用字典存储后重组数组 self.activeUsers = fetchedUsers self.activeUsersTableView.reloadData() } } }
方案2:只刷新新增的行
放弃全量reloadData(),改用insertRows(at:with:)仅刷新新增的行,避免重复调用之前的cellForRowAt:
func fetchActiveUsers() { guard let chatsDocRef = self.chatsDocRef else { return } chatsDocRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") return } guard let document = document, let data = document.data(), let activeUserUIDS = data["users"] as? [String] else { return } self.activeUserUIDS = activeUserUIDS var usersDict = [String: UserProfile]() let totalUsers = activeUserUIDS.count for uid in activeUserUIDS { let userDocRef = Firestore.firestore().collection("users").document(uid) UserProfile.getUserProfileData(userDocRef) { user in usersDict[uid] = user // 所有请求完成后按顺序重组数组并刷新 if usersDict.count == totalUsers { self.activeUsers = activeUserUIDS.compactMap { usersDict[$0] } self.activeUsersTableView.reloadData() } else { // 无需严格顺序时,直接插入新增行 DispatchQueue.main.async { self.activeUsers.append(user) let newIndexPath = IndexPath(row: self.activeUsers.count - 1, section: 0) self.activeUsersTableView.insertRows(at: [newIndexPath], with: .automatic) } } } } } }
方案3:串行执行请求
若必须按activeUserUIDS的顺序获取用户数据,可通过递归实现串行请求,保证顺序一致:
func fetchActiveUsers() { guard let chatsDocRef = self.chatsDocRef else { return } chatsDocRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") return } guard let document = document, let data = document.data(), let activeUserUIDS = data["users"] as? [String] else { return } self.activeUserUIDS = activeUserUIDS self.fetchUser(at: 0, from: activeUserUIDS) } } private func fetchUser(at index: Int, from uids: [String]) { guard index < uids.count else { self.activeUsersTableView.reloadData() return } let uid = uids[index] let userDocRef = Firestore.firestore().collection("users").document(uid) UserProfile.getUserProfileData(userDocRef) { user in self.activeUsers.append(user) // 可选:每新增一个就刷新,或最后统一刷新 DispatchQueue.main.async { self.activeUsersTableView.reloadData() } self.fetchUser(at: index + 1, from: uids) } }
内容的提问来源于stack exchange,提问作者BiggRojo

