Mark L
Mark L

Reputation: 759

swift crash when altering UITableViewCells during update

I'm trying to fix a bug where my app is crashing when tapping on a cell while it's trying to fetch new data for the UITableView.

Here's how my data-flow is set up.

When the viewController is loaded, I load data from CoreData, and fill my UITableView with that data (because it's saved on the phone this happens instantly).

However at the end of my load function, I run another function to update the data. If i interact with my UITableView at any point while this function is running, my app crashes.

I seem to be getting several different errors, when I attempt to recreate the bug:

Error 1:

malloc: *** error for object 0x17001b830: Invalid pointer dequeued from free list
*** set a breakpoint in malloc_error_break to debug

Error 2:

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1623
2015-05-27 04:40:30.250 Stocks[2279:375070] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (1 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
(0x182cc82d8 0x1944f40e4 0x182cc8198 0x183b7ced4 0x1878eeec8 0x100100a88 0x100100b7c 0x187831474 0x1878eb790 0x18778c240 0x1876fc6ec 0x182c802a4 0x182c7d230 0x182c7d610 0x182ba92d4 0x18c3bf6fc 0x18776efac 0x1001783b8 0x194b72a08)
libc++abi.dylib: terminating with uncaught exception of type NSException

I have a good idea where the error is originating from. But I'm clueless on how to overcome this issue.

I'm 95% sure the problem lies in how I update my UITableView data.

func updatePortfolio() {
    println("Updating Portfolio")

    // Check for an internet connection.
    if Reachability.isConnectedToNetwork() == false {
        println("ERROR: - No Internet Connection")
    } else {
        // Delete all the current objects in the dataset
        let fetchRequest = NSFetchRequest(entityName: formulaEntity)
        let a = managedContext.executeFetchRequest(fetchRequest, error: nil) as! [NSManagedObject]
        for mo in a {
            managedContext.deleteObject(mo)
        }
        // Removing them from the array
        stocks.removeAll(keepCapacity: false)
        // Saving the now empty context.
        managedContext.save(nil)

        // Set up a fetch request for the API data
        let entity =  NSEntityDescription.entityForName(formulaEntity, inManagedObjectContext:managedContext)
        var request = NSURLRequest(URL: formulaAPI!)
        var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
        var formula = JSON(data: data!)

        // Loop through the api data.
        for (index: String, portfolio: JSON) in formula["portfolio"] {

            // Save the data into temporary variables
            stockName = portfolio["name"].stringValue.lowercaseString.capitalizedString
            ticker = portfolio["ticker"].stringValue
            purchasePrice = portfolio["purchase_price"].floatValue
            weight = portfolio["percentage_weight"].floatValue
            latestAPIPrice = portfolio["latest_price"].floatValue
            daysHeld = portfolio["days_owned"].intValue

            // Set up CoreData for inserting a new object.
            let stock = NSManagedObject(entity: entity!,insertIntoManagedObjectContext:managedContext)

            // Save the temporary variables into coreData
            stock.setValue(stockName, forKey: "name")
            stock.setValue(ticker, forKey: "ticker")
            stock.setValue(action, forKey: "action")
            stock.setValue(purchasePrice, forKey: "purchasePrice")
            stock.setValue(weight, forKey: "weight")
            stock.setValue(daysHeld, forKey: "daysHeld")

            // Doing a bunch of API calls here, (they take up a lot of space and probably isn't relevant to the issue, so I cut it off for readability.

            // If no data is found from the APIs use the last price from our own API.
            if lastPrice == 0 {
                lastPrice = latestAPIPrice
            }


            // This can simply be set, because it will be 0 if not found.
            stock.setValue(lastPrice, forKey: "lastPrice")

            // Error handling
            var error: NSError?
            if !managedContext.save(&error) {
                println("Could not save \(error), \(error?.userInfo)")
            }
            // Append the object to the array. Which fills the UITableView
            stocks.append(stock)
        }

        NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
            // Resize the UITableView to match.
            self.tableHeight.constant = CGFloat(self.stocks.count*50)
            self.pHoldingsTable.reloadData()
            self.scrollView.contentSize = CGSize(width:self.view.bounds.width, height: self.contentView.frame.height)
            println("Finished Updating Portfolio")

        })
    }
}

I'm starting off the above function with deleting everything in the CoreData. This is probably where I'm going wrong. Since my UITableView is filled using that same CoreData. Removing it most likely causes issues with the UITableView.

I've been trying to think of a better way to do this. But I need to add my new data to the same model. If I just add it, to what's already there. I now have no way to delete the old data.

So how can you update data, while it's currently being used?

The update function takes several seconds to run (on a good connection), so the user needs to be able to interact with the UITableView while this function is running in the background.

I've looked for quite a while, but I can only find examples. Where they interrupt the users experience with a loading screen until it's done. But because I'm doing so many API calls, it just takes way to long to be a good experience waiting for. And most of the time the data won't change much if at all.

Upvotes: 1

Views: 1768

Answers (1)

Quentin Hayot
Quentin Hayot

Reputation: 7876

The problem is that your datasource must be consistent with your TableView.
When you scroll your TableView, each row will be pulled from your datasource every time they are about to appear on screen.
When you call stocks.removeAll(keepCapacity: false), you should also reload your TableView to keep them synced.

EDIT: If you want to keep a good user experience while it loads:

  • Clear the table view and display an UIActivityIndicator that will show that the app is busy
    or
  • Use a temporary array to fetch your data and then switch your datasource to it when you are done. That way, the user will still be able to see the "old" data while the app is working on fetching the new data.

Upvotes: 2

Related Questions