Skip to content

UITableView Causing NSInternalInconsistencyException #617

@tccpg288

Description

@tccpg288

I have a leaderboard in my app and the data is stored in Firebase Firestore. The leaderboard dynamically changes based on events related to the users.

Every so often, the leaderboard is crashing and I am receiving an NSInternalInconsistencyException. I am unsure why, however it may have to do with when the data in Firebase dynamically changes and the UITableView repopulates the data. Below is the associated code:

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)
    }
}

Output:

enter image description here

LeaderboardViewController.Swift (where error is occurring):

 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)
    }
    
}

Output:

enter image description here

Exception:

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) 

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions