Reputation: 759
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
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:
Upvotes: 2