我的应用程序中有一个排行榜,数据存储在Firebase Firestore中。排行榜会根据与用户相关的事件动态更改。
每隔很多次,排行榜就崩溃了,我收到了NSInternalInconsistencyException
。我不确定为什么,但是当Firebase中的数据动态更改并且UITableView
重新填充数据时,可能与它有关。下面是相关的代码:
LeadersViewController.Swift
class LeadersViewController: UIViewController {
private let ROWS_TO_SHOW = 3
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var cashierButton: UIButton!
@IBOutlet weak var allTimeTableCard: TableCard!
@IBOutlet weak var thisMonthTableCard: TableCard!
@IBOutlet weak var lastMonthTableCard: TableCard!
fileprivate var allTimeDataSource: FUIFirestoreTableViewDataSource!
fileprivate var thisMonthDataSource: FUIFirestoreTableViewDataSource!
fileprivate var lastMonthDataSource: FUIFirestoreTableViewDataSource!
var userListener: ListenerRegistration!
var allTimeListener: ListenerRegistration!
var thisMonthListener: ListenerRegistration!
var lastMonthListener: ListenerRegistration!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.userListener = FirestoreUtil.loadUser { (object) in
guard let user: User = object else {
return
}
self.nameLabel.text = user.displayName ?? ""
let cash = OddsUtil.formatCash(cashAmount: user.balanceNumeric)
self.cashierButton.setTitle(cash, for: .normal)
}
setupAllTimeStat()
setupThisMonthStat()
setupLastMonthStat()
}
override func viewWillDisappear(_ animated: Bool) {
userListener.remove()
allTimeDataSource.unbind()
allTimeDataSource.tableView = nil
allTimeDataSource = nil
allTimeTableCard.tableView.dataSource = nil
allTimeTableCard.tableView.reloadData()
thisMonthDataSource.unbind()
thisMonthDataSource.tableView = nil
thisMonthDataSource = nil
thisMonthTableCard.tableView.dataSource = nil
thisMonthTableCard.tableView.reloadData()
lastMonthDataSource.unbind()
lastMonthDataSource.tableView = nil
lastMonthDataSource = nil
lastMonthTableCard.tableView.dataSource = nil
lastMonthTableCard.tableView.reloadData()
allTimeListener.remove()
thisMonthListener.remove()
lastMonthListener.remove()
super.viewWillDisappear(animated)
}
func setupAllTimeStat() {
allTimeTableCard.headerImageView.image = UIImage(named: "ic_format_list_numbered_black_18dp")
allTimeTableCard.headerLabel.text = "Profit: All-Time"
allTimeTableCard.bottomView.isHidden = false
allTimeTableCard.bottomLabel.text = "VIEW LEADERBOARD"
self.allTimeTableCard.tableView.register(UINib(nibName: "LeaderTableViewCell", bundle: nil),
forCellReuseIdentifier: "LeaderTableViewCell")
self.allTimeTableCard.tableView.delegate = self
self.allTimeTableCard.tableView.tableFooterView = UIView(frame: CGRect.zero)
let query = Firestore.firestore()
.collection("userStats")
.whereField("timePeriodString", isEqualTo: "ALLTIME")
.whereField("statType", isEqualTo: StatType.AMOUNT_NETTED.rawValue)
.whereField("valueAsDouble", isGreaterThan: 0)
.order(by:"valueAsDouble", descending: true)
.limit(to: ROWS_TO_SHOW)
self.allTimeListener = query.addSnapshotListener { (snapshot, error) in
DispatchQueue.main.async {
self.allTimeTableCard.tableView.reloadData()
}
}
self.allTimeDataSource = allTimeTableCard.tableView.bind(toFirestoreQuery: query, populateCell: { (tableView, indexPath, snapshot) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderTableViewCell", for: indexPath) as! LeaderTableViewCell
let stat = UserStat(dict: snapshot.data() ?? [:])
cell.setStat(position: indexPath.row, userStat: stat)
return cell
})
self.allTimeTableCard.viewAll = {
LeaderboardViewController.openLeaderboard(sender: self, leaderboardTitle: "Profit: All-Time", period: "ALLTIME")
}
self.allTimeTableCard.tableView.reloadData()
}
func setupThisMonthStat() {
thisMonthTableCard.headerImageView.image = UIImage(named: "ic_format_list_numbered_black_18dp")
thisMonthTableCard.headerLabel.text = "Profit: \(DateUtil.formatMonthYear())"
thisMonthTableCard.bottomView.isHidden = false
thisMonthTableCard.bottomLabel.text = "VIEW LEADERBOARD"
self.thisMonthTableCard.tableView.register(UINib(nibName: "LeaderTableViewCell", bundle: nil),
forCellReuseIdentifier: "LeaderTableViewCell")
self.thisMonthTableCard.tableView.delegate = self
self.thisMonthTableCard.tableView.tableFooterView = UIView(frame: CGRect.zero)
let period = "\(DateUtil.year())\(DateUtil.month() - 1)"
let query = Firestore.firestore()
.collection("userStats")
.whereField("timePeriodString", isEqualTo: period)
.whereField("statType", isEqualTo: StatType.AMOUNT_NETTED.rawValue)
.whereField("valueAsDouble", isGreaterThan: 0)
.order(by:"valueAsDouble", descending: true)
.limit(to: ROWS_TO_SHOW)
self.thisMonthListener = query.addSnapshotListener { (snapshot, error) in
DispatchQueue.main.async {
self.thisMonthTableCard.tableView.reloadData()
}
}
self.thisMonthDataSource = thisMonthTableCard.tableView.bind(toFirestoreQuery: query, populateCell: { (tableView, indexPath, snapshot) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderTableViewCell", for: indexPath) as! LeaderTableViewCell
let stat = UserStat(dict: snapshot.data() ?? [:])
cell.setStat(position: indexPath.row, userStat: stat)
return cell
})
self.thisMonthTableCard.viewAll = {
LeaderboardViewController.openLeaderboard(sender: self, leaderboardTitle: "Profit: \(DateUtil.formatMonthYear())", period: period)
}
self.thisMonthTableCard.tableView.reloadData()
}
func setupLastMonthStat() {
let previousMonth = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date()
lastMonthTableCard.headerImageView.image = UIImage(named: "ic_format_list_numbered_black_18dp")
lastMonthTableCard.headerLabel.text = "Profit: \(DateUtil.formatMonthYear(date: previousMonth))"
lastMonthTableCard.bottomView.isHidden = false
lastMonthTableCard.bottomLabel.text = "VIEW LEADERBOARD"
self.lastMonthTableCard.tableView.register(UINib(nibName: "LeaderTableViewCell", bundle: nil),
forCellReuseIdentifier: "LeaderTableViewCell")
self.lastMonthTableCard.tableView.delegate = self
self.lastMonthTableCard.tableView.tableFooterView = UIView(frame: CGRect.zero)
let period = "\(DateUtil.year(date: previousMonth))\(DateUtil.month(date: previousMonth) - 1)"
let query = Firestore.firestore()
.collection("userStats")
.whereField("timePeriodString", isEqualTo: period)
.whereField("statType", isEqualTo: StatType.AMOUNT_NETTED.rawValue)
.whereField("valueAsDouble", isGreaterThan: 0)
.order(by:"valueAsDouble", descending: true)
.limit(to: ROWS_TO_SHOW)
self.lastMonthListener = query.addSnapshotListener { (snapshot, error) in
DispatchQueue.main.async {
self.lastMonthTableCard.tableView.reloadData()
}
}
self.lastMonthDataSource = lastMonthTableCard.tableView.bind(toFirestoreQuery: query, populateCell: { (tableView, indexPath, snapshot) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderTableViewCell", for: indexPath) as! LeaderTableViewCell
let stat = UserStat(dict: snapshot.data() ?? [:])
cell.setStat(position: indexPath.row, userStat: stat)
return cell
})
self.lastMonthTableCard.viewAll = {
LeaderboardViewController.openLeaderboard(sender: self, leaderboardTitle: "Profit: \(DateUtil.formatMonthYear(date: previousMonth))", period: period)
}
self.lastMonthTableCard.tableView.reloadData()
}
@IBAction func cashier(_ sender: Any) {
CashierViewController.openCashier(sender: self)
}
}
extension LeadersViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath:
IndexPath) {
let userId = (tableView.cellForRow(at:
tableView.indexPathForSelectedRow!) as!
LeaderTableViewCell).userStat?.userId ?? ""
ProfileViewController.openPorfile(vc: self, userId: userId)
}
}
输出:
LeaderboardViewController.Swift(发生错误的地方):
import UIKit
import FirebaseUI
class LeaderboardViewController: UIViewController {
private let ROWS_TO_SHOW = 100
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var indicator: UIActivityIndicatorView!
fileprivate var dataSource: FUIFirestoreTableViewDataSource!
var listener: ListenerRegistration!
var leaderboardTitle: String!
var period: String!
override func viewDidLoad() {
super.viewDidLoad()
title = leaderboardTitle
self.tableView.register(UINib(nibName: "LeaderTableViewCell", bundle: nil),
forCellReuseIdentifier: "LeaderTableViewCell")
self.tableView.tableFooterView = UIView(frame: CGRect.zero)
self.tableView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let query = Firestore.firestore()
.collection("userStats")
.whereField("timePeriodString", isEqualTo: period)
.whereField("statType", isEqualTo: StatType.AMOUNT_NETTED.rawValue)
.order(by:"valueAsDouble", descending: true)
.limit(to: ROWS_TO_SHOW)
self.listener = query.addSnapshotListener { (snapshot, error) in
DispatchQueue.main.async {
if snapshot?.count ?? 0 > 3 {
self.indicator.stopAnimating()
}
self.tableView.reloadData()
}
}
self.dataSource = tableView.bind(toFirestoreQuery: query, populateCell: { (tableView, indexPath, snapshot) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderTableViewCell", for: indexPath) as! LeaderTableViewCell
let stat = UserStat(dict: snapshot.data() ?? [:])
cell.setStat(position: indexPath.row, userStat: stat)
return cell
})
self.tableView.reloadData()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
dataSource.unbind()
dataSource.tableView = nil
dataSource = nil
tableView.dataSource = nil
tableView.reloadData()
listener.remove()
}
}
extension LeaderboardViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let userId = (tableView.cellForRow(at: tableView.indexPathForSelectedRow!) as! LeaderTableViewCell).userStat?.userId ?? ""
ProfileViewController.openPorfile(vc: self, userId: userId)
}
}
extension LeaderboardViewController {
public static func openLeaderboard(sender: UIViewController, leaderboardTitle: String, period: String) {
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "LeaderboardViewController") as! LeaderboardViewController
vc.leaderboardTitle = leaderboardTitle
vc.period = period
sender.navigationController?.pushViewController(vc, animated: true)
}
}
输出:
例外:
2019-02-06 21:15:49.293694-0600 BetShark[18200:155059] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3698.93.8/UITableView.m:1776
2019-02-06 21:15:49.316429-0600 BetShark[18200:155059] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to perform an insert and a move to the same index path (<NSIndexPath: 0xc55d03bd8e95ff5b> {length = 2, path = 0 - 68})'
*** First throw call stack:
(
0 CoreFoundation 0x00000001116921bb __exceptionPreprocess + 331
1 libobjc.A.dylib 0x0000000110c30735 objc_exception_throw + 48
2 CoreFoundation 0x0000000111691f42 +[NSException raise:format:arguments:] + 98
3 Foundation 0x000000010c8e1877 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 194
4 UIKitCore 0x000000011a47688a -[UITableView _endCellAnimationsWithContext:] + 9355
5 UIKitCore 0x000000011a492711 -[UITableView endUpdates] + 75
6 BetShark 0x0000000108a0095c -[FUIFirestoreTableViewDataSource batchedArray:didUpdateWithDiff:] + 2321
7 BetShark 0x00000001089f44f8 __31-[FUIBatchedArray observeQuery]_block_invoke + 658
8 BetShark 0x00000001088e7fdc __60-[FIRQuery addSnapshotListenerInternalWithOptions:listener:]_block_invoke + 197
9 BetShark 0x00000001088d5e68 _ZZN8firebase9firestore4util8internal13DispatchAsyncEPU28objcproto17OS_dispatch_queue8NSObjectONSt3__18functionIFvvEEEEN3$_08__invokeEPv + 14
10 libdispatch.dylib 0x0000000112885602 _dispatch_client_callout + 8
11 libdispatch.dylib 0x000000011289299a _dispatch_main_queue_callback_4CF + 1541
12 CoreFoundation 0x00000001115f73e9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
13 CoreFoundation 0x00000001115f1a76 __CFRunLoopRun + 2342
14 CoreFoundation 0x00000001115f0e11 CFRunLoopRunSpecific + 625
15 GraphicsServices 0x0000000113da71dd GSEventRunModal + 62
16 UIKitCore 0x000000011a27081d UIApplicationMain + 140
17 BetShark 0x0000000108722052 main + 50
18 libdyld.dylib 0x00000001128fb575 start + 1
19 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)