Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UITableView Causing NSInternalInconsistencyException #617

Closed
tccpg288 opened this issue Feb 13, 2019 · 9 comments
Closed

UITableView Causing NSInternalInconsistencyException #617

tccpg288 opened this issue Feb 13, 2019 · 9 comments
Assignees

Comments

@tccpg288
Copy link

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) 
@tccpg288 tccpg288 changed the title UITabelView Causing NSInternalInconsistencyException UITableView Causing NSInternalInconsistencyException Feb 13, 2019
@morganchen12
Copy link
Contributor

Does the crash still reproduce if you remove the call to tableView.reloadData() in the query snapshot listener?

@morganchen12 morganchen12 self-assigned this Feb 13, 2019
@tccpg288
Copy link
Author

If I do that, the number/ranking in the second screenshot does not update in conjunction with the other view items.

@morganchen12
Copy link
Contributor

You should be updating all of the table view cell data in the data source callback.

@tccpg288
Copy link
Author

tccpg288 commented Feb 14, 2019

Got it - why do you believe that is causing the exception though? It appears that there are some issues with multiple rows populating the data. In this case, it is row 68.

@morganchen12
Copy link
Contributor

morganchen12 commented Feb 14, 2019

I'm not 100% convinced it's the source, but it could potentially be updating table view state while FirestoreUI is animating updates, which could then cause an NSInternalInconsistencyException crash.

Alternatively, you can avoid using FirestoreUI's automatic updates and instead manually pull data from Firestore and call tableView.reloadData() to update the UI.

If you're able to isolate the crash and send me a reproducible project, I can take a look further.

@tccpg288
Copy link
Author

Got it - what about disabling offline persistence? Would that be an option as well so that it does not cache the previous state of the leaderboard and always creates a fresh load of the data?

@morganchen12
Copy link
Contributor

That shouldn't affect things, since FUIFirestoreArray is designed to not be able to tell the difference between a cached load and fresh backend data.

@morganchen12
Copy link
Contributor

This should be fixed by #689.

@morganchen12
Copy link
Contributor

The fix has been released. The table view cells should update correctly without you having to manually insert update calls that may be crashy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants