I just do the normal operation: Request the data from the server and refresh the tableview by reloadSections, but I do get a lot of crash logs. If I call reloadData, everything is OK. Even though I known reloadData works well, it seems that reloadSections works more efficiently.
Does anybody have any ideas?
Get data from the server by persistent connection :
[_pomelo onRoute:#"onTlive" withCallback:^(NSDictionary *data) {
#strongify(self);
if (self.liveTableView) {
[self.liveTableView configData:data refresh:self.playerView.isScreenPortrait];
}
}];
Then config the data and refresh the tableview:
- (void)configData:(NSDictionary *)data refresh:(BOOL)refresh {
if (!data || data.allKeys.count == 0) {
return;
}
NSArray *liveArray = [data valueForKeyPath:#"body.data"];
[liveArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSArray *liveDetailInfo = [obj componentsSeparatedByString:#"|"];
if (liveDetailInfo && liveDetailInfo.count > 0) {
NSArray *tliveInfo = [liveDetailInfo.lastObject componentsSeparatedByString:#"^"];
SPBasketballLive *liveObject = [SPBasketballLive new];
liveObject.type = tliveInfo[0];
liveObject.time = tliveInfo[1];
liveObject.team = tliveInfo[2];
liveObject.playerId = tliveInfo[3];
liveObject.score = tliveInfo[4];
liveObject.text = tliveInfo[5];
liveObject.quarter = ((NSString *)liveDetailInfo.firstObject).integerValue;
if (self.curMatch.liveInfo.count == liveObject.quarter) {
NSMutableArray *quarterArray = self.curMatch.liveInfo.firstObject;
[quarterArray insertObject:liveObject atIndex:0];
} else {
NSMutableArray *quarterArray = [NSMutableArray new];
[quarterArray addObject:liveObject];
[self.curMatch.liveInfo insertObject:quarterArray atIndex:0];
}
}
}];
if (refresh) {
if (_curMatch.liveInfo.count > 0) {
[self.myTableView reloadSections:[NSIndexSet indexSetWithIndex:4] withRowAnimation:UITableViewRowAnimationNone];
}
}
}
Here is the crash log:
invalid update: invalid number of rows in section 4. The number of rows contained in an existing section after the update (98) must be equal to the number of rows contained in that section before the update (96), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out)
Your app crashed because reloadSection method won't update section and row counters so it causes crashes in cases when you changed section or row counter and your UITableView have different counters.
And when you use reloadData it first call numberOfSectionsInTableView and then numberOfRowsInSection is called so your UITableView has correct counters for sections and rows.
Related
Hello: I'm using a collection view within my app, and I've noticed that it's taking longer than expected to refresh using reloadData. My collection view has 1 section, and I'm testing it with 5 cells (each of which has 2 buttons and a label). I put some logs into my code to show how long the system is actually taking to refresh. Interestingly enough, the logs indicate that it's refreshing faster than it is. On a device, for example, it will take up to ~0.2sec (noticeable), but here are the logs:
0.007s From the time reloadData is called to the time cellForItemAtIndexPath is called the first time
0.002s Per cell to load and be returned
0.041s From the time reloadData is called to the time where cell #5 is returned
There isn't anything particularly intensive in the cellForItemAtIndexPath function (basically just finds a dictionary with 3 values within an NSArray at the indexPath's row). Even when I removed this and just returned a cell with a blank button, I saw the same behavior, however.
Does anyone have any idea as to why this may be happening? It's only happening on a physical device (iPad Air), by the way. Thanks!
EDIT #1
Per #brian-nickel's comment, I used the Time Profiler instrument, and found that it does indeed spike each time reloadData is called. Here's a screenshot:
#ArtSabintsev, here is the function surrounding the reloadData call, followed by the cellForItemAtIndexPath:
//Arrays were just reset, load new data into them
//Loop through each team
for (NSString *team in moveUnitsView.teamsDisplaying) { //CURRENT TEAM WILL COME FIRST
//Create an array for this team
NSMutableArray *teamArr = [NSMutableArray new];
//Loop through all units
for (int i = [Universal units]; i > 0; i--) {
//Set the unit type to a string
NSString *unitType = [Universal unitWithTag:i];
//Get counts depending on the team
if ([team isEqualToString:currentTeam.text]) {
//Get the number of units of this type so that it supports units on transports. If the territory is a sea territory and the current unit is a ground unit, check the units in the transports instead of normal units
int unitCount = (ter.isSeaTerritory && (i == 1 || i == 2 || i == 8)) ? [self sumOfUnitsInTransportsOfType:unitType onTerritory:ter onTeam:team] : [ter sumOfUnitsOfType:unitType onTeam:team];
//Get the number of movable units on this territory
int movableCount = 0;
if (queue.selectedTerr != nil && queue.selectedTerr != ter) { //This is here to prevent the user from selecting units on another territory while moving units from one territory
movableCount = 0;
} else if (ter.isSeaTerritory && (i == 1 || i == 2 || i == 8)) { //Units on transports - can be an enemy territory
movableCount = [self sumOfUnitsInTransportsOfType:unitType onTerritory:ter onTeam:team];
} else if ([Universal allianceExistsBetweenTeam:team andTeam:ter.currentOwner] || i == 3 || i == 9) { //Other units - only planes can be on an enemy territory
movableCount = [ter sumOfMovableUnitsOfType:unitType onTeam:team];
}
//See if there are units of this type on this territory on this team
if (unitCount > 0) {
//Add data to this team's dictionary
NSMutableDictionary *unitInfo = [NSMutableDictionary new];
[unitInfo setObject:#(i) forKey:#"UnitTag"];
[unitInfo setObject:unitType forKey:#"UnitType"];
[unitInfo setObject:#(unitCount) forKey:#"Count"];
[unitInfo setObject:#(movableCount) forKey:#"MovableCount"];
[unitInfo setObject:team forKey:#"Team"];
//Add the dictionary
[teamArr addObject:unitInfo];
//Increment the counter
if (unitsOnCT) { //Must check or it could cause a crash
*unitsOnCT += 1;
}
}
}
}
//Add the team array
[moveUnitsView.unitData addObject:teamArr];
}
//Reload the data in the collection view
[moveUnitsView.collectionV reloadData];
And my cellForItemAtIndexPath's relevant code:
//Dequeue a cell
UnitSelectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"UnitSelectionCell" forIndexPath:indexPath];
//Get the team array (at the index of the section), then the unit's data (at the index of the row)
NSMutableDictionary *unitData = (moveUnitsView.unitData[indexPath.section])[indexPath.row];
//Get values
int unitTag = [[unitData objectForKey:#"UnitTag"] intValue];
int count = [[unitData objectForKey:#"Count"] intValue];
int movableCount = [[unitData objectForKey:#"MovableCount"] intValue];
NSString *unitType = [unitData objectForKey:#"UnitType"];
//Set the cell's values
[cell.upB addTarget:self action:#selector(upMoveUnits:) forControlEvents:UIControlEventTouchUpInside]; [cell.upB setTag:unitTag];
[cell.iconB setBackgroundImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[Universal imageNameForUnit:unitType team:[unitData objectForKey:#"Team"]] ofType:nil]] forState:UIControlStateNormal];
[cell.iconB setTitle:[Universal strForExpDisplay:count] forState:UIControlStateNormal];
[Universal adjustTitlePlacementOfB:cell.iconB autosize:FALSE]; //Don't autosize because this is a collection view
cell.unitTypeL.text = unitType;
cell.unitTypeL.adjustsFontSizeToFitWidth = cell.unitTypeL.adjustsLetterSpacingToFitWidth = TRUE;
//Set fonts
[Universal setFontForSubviewsOfView:cell];
//Return the cell
return cell;
When the collection view is initialized, cells are registered using:
[moveUnitsView.collectionV registerNib:[UINib nibWithNibName:#"UnitSelectionCell" bundle:nil] forCellWithReuseIdentifier:#"UnitSelectionCell"];
EDIT #2
#roycable and #aaron-brager pointed out that this could be caused by using imageWithContentsOfFile:. To test this out, I changed cellForItemAtIndexPath to this:
//Dequeue a cell
UnitSelectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"UnitSelectionCell" forIndexPath:indexPath];
//Get the team array (at the index of the section), then the unit's data (at the index of the row)
NSMutableDictionary *unitData = (moveUnitsView.unitData[indexPath.section])[indexPath.row];
//Get values
int unitTag = [[unitData objectForKey:#"UnitTag"] intValue];
[cell setBackgroundColor:[UIColor redColor]];
[cell.upB removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside];
[cell.upB addTarget:self action:#selector(upMoveUnits:) forControlEvents:UIControlEventTouchUpInside]; [cell.upB setTag:unitTag];
//Return the cell
return cell;
Strangely, this doesn't seem to fix the issue. It's literally doing no intensive tasks in that function but it still seems to be lagging (and the Time Profiler seems to confirm that).
In response to the requests for code from Universal, instead of posting code I'll just summarize it:
+units just returns 17
+unitWithTag: uses a switch to return an NSString corresponding to a number between 1-17
+allianceExistsBetweenTeam: sees if an array contains one of the strings
+setFontForSubviewsOfView: is a recursive function that basically uses this code
Unfortunately, this doesn't seem very relevant since the issue is still occurring with the oversimplified cellForItemAtIndexPath function.
I also implemented #aaron-brager's new suggestions. I removed the target before adding a new one, and I made the changes to Time Profiler. I didn't see anything really pop out... Here's the screenshot. Everything related to UIImage is irrelevant to this question, as is NSKeyedArchiver, so the only other things that really make sense are strings, arrays, and dictionaries:
Any and all help is greatly appreciated - I really need to get this fixed (hence the bounty). Thank you!
Edit #3 - Solution identified
So, it turns out that the issue wasn't in either of those functions. The issue was the function (let's call it Function A) that called the update function above (let's call it Function B). Right after Function A called Function B, it performed a CPU-intensive task. I wasn't aware of the fact that reloadData is at least partially asynchronous, so I'm assuming the CPU-intensive task and reloadData ended up racing for CPU time. I solved my problem by adding the following right before return cell;:
if (indexPath.row == [self collectionView:collectionView numberOfItemsInSection:indexPath.section] - 1) {
[self performSelector:#selector(performMyCPUIntensiveTask:) withObject:myObject afterDelay:0.1];
}
I hope this helps someone else in the future. Thank you to everyone who helped, I sincerely appreciate it.
Make sure you're on the main thread when you call reloadData.
NSLog("on main thread: %#", [NSThread isMainThread] ? #"YES" : #"NO");
If you're not then use GCD to send the message on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[moveUnitsView.collectionV reloadData];
});
(Not 100% sure about syntax, I just typed this into the browser)
Some possibilities:
Your presumably recursive function to set the fonts is probably expensive.
A few of the other Universal functions look like they might be expensive.
It does not appear that you ever remove the button target and every time a cell is reused, you are adding additional targets to it.
imageWithContentsOfFile: skips the cache; use imageNamed: instead.
so I'm creating a contact list app and I'm storing everything into an NSMutableArray. I've managed to get add function working perfectly. But I'm struggling with the Previous/Next functions.
I can get it to go back one object in the array when pressing previous, but i Can't get it to go back any further? here's my code: I have two extra classes, PhoneBookEntry which is a subclass of the Person class. These classes contain three strings, Firstname,lastname and studentID
- (IBAction)addPerson:(id)sender {
PhonebookEntry *person = [[PhonebookEntry alloc] init];
NSLog(#"%#",self.firstName.text);
person.firstName = self.firstName.text;
person.lastName = self.lastName.text;
person.phoneNumber = self.phoneNumber.text;
[self.entries addObject:person];
Here's my Previous button:
int length;
length = [self.entries count];
if (length > 0) {
int index;
index = [self.entries count] - 1;
NSLog (#"%d the index is", index);
NSLog(#"object %#", [self.entries objectAtIndex:index]);
} else {
NSLog (#"No contacts have been entered. No");
}
//NSLog(#"%d is the length - 1 hopefully", length);
//NSLog (#"index at %d is ", length);
I've tried removing the - 1 here:
index = [self.entries count] - 1;
and changing then on the next line putting index--; but nothing seems to work. it just goes back once.
I understand that length is getting the amount of objects in the index, and then -1 but shouldnt i-- at the end of the count / - 1 keep removing it everytime its pressed??
Any ideas? Cheers
You're going to keep hitting the same index with that piece of code - you need to store the state somewhere. Who is going to hold onto what the current index is? With a good designed system the model should really take care of this.
You are best off storing the current index into an ivar and updating that every time the button is pressed.
#interface OKAClass ()
#property (nonatomic, assign) NSInteger currentIndex;
#end
//... initialise the property with a default value
self.currentIndex = [self.entries count] - 1;
//... when the button is pressed decrement the index (you might want some min / max validation or use modulus to loop over and start from the top again)
NSLog(#"%#", self.entries[self.currentIndex--]);
so I'm creating a contact list app and I'm storing everything into an NSMutableArray. I've managed to get add function working perfectly. But I'm struggling with the Previous/Next functions.
I can get it to go back one object in the array when pressing previous, but i Can't get it to go back any further? here's my code: I have two extra classes, PhoneBookEntry which is a subclass of the Person class. These classes contain three strings, Firstname,lastname and studentID
- (IBAction)addPerson:(id)sender {
PhonebookEntry *person = [[PhonebookEntry alloc] init];
NSLog(#"%#",self.firstName.text);
self.currentIndex++;
person.firstName = self.firstName.text;
person.lastName = self.lastName.text;
person.phoneNumber = self.phoneNumber.text;
[self.entries addObject:person];
NSLog(#"%#", self.entries);
[self arrayLength ];
I've created a property to hold the current index.
#property (nonatomic, assign) NSInteger currentIndex;
I've created a method which then -1 from the currentINdex;
self.currentIndex = [self.entries count] - 1;
heres my button previous.
- (IBAction)btnPrevious:(id)sender {
//[self prevObject];
int length;
length = [self.entries count];
if (length > 0) {
NSLog(#"%#", self.entries[self.currentIndex--]);
// NSLog(#"object %#", [self.entries objectAtIndex:index]);
}
else {
NSLog (#"No contacts have been entered. No");
}
I can get it to go back once, but when I press it a second time Iget the following error.
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
Theres definitely data in there as I get it to print whats in the array, it also prints when I press previous also. happens no matter how many times I press the add button.
thanks
Better:
Previous Index:
// in order to go back you need to make sure that you're not already
// on index 0 and that you've items to go back to
if([self.entries count] > 1 && self.currentIndex > 0) {
self.currentIndex--;
}
Next Index:
// in order to go forward you need to make sure that you're not already
// on the last element
if(self.currentIndex < ([self.entries count] - 1)) {
self.currentIndex++;
}
Some further explanation on the root cause of your problem:
The main problem is, that you use currentIndex--. This will use the old currentIndex (which is 2) before decreasing the value. If you want to use the decreased value in this statement, you have to apply the decreasing before, i.e. by writing --currentIndex instead.
--expression = pre-decrement operator
expression-- = post-decrement operator
The pre-decrement operator does decrease the value and return the decreased value while the post-decrement operator will decrease the value by 1 and return the initial value.
i think you are starting from end of list
self.currentIndex = [self.entries count] - 1;
if current index is # 1 and you have 2 items in array adding an other will give you error
try this
NSLog(#"%#", self.entries[self.currentIndex--]);
For Previous
- (IBAction)btnPrevious:(id)sender
{
if(self.currentIndex <= 0)
{
self.currentIndex = [self.entries count] - 1;
NSLog(#"%#", self.entries[self.currentIndex])
}
else
{
self.currentIndex --;
NSLog(#"%#", self.entries[self.currentIndex])
}
}
For Next
- (IBAction)btnNext:(id)sender
{
if(self.currentIndex >= [self.entries count] - 1)
{
self.currentIndex = 0;
NSLog(#"%#", self.entries[self.currentIndex])
}
else
{
self.currentIndex ++;
NSLog(#"%#", self.entries[self.currentIndex])
}
}
Hope this will working for you.
I've created the master-detail view app using xcode and let xcode generate all the things it should do. I added a few buttons to the TableView and finally hooked up a responder method to these buttons: looking something like this (not all code pasted):
-(IBAction)orderBy:(UIBarButtonItem *)sender
{
int lv_temp = sender.tag;
choice = lv_temp;
switch (lv_temp) {
case 0:
break;
case 1:
{
//This gives us all the different sections available.
typeSections = [Items valueForKeyPath:#"#distinctUnionOfObjects.type"];
//Do some more stuff and create the Dictionary so we hold an array with a key for each section
[ItemsByType2 setObject:ItemsTemp forKey:[NSString stringWithFormat:#"%#", id]];
[ItemsTemp removeAllObjects];
}
The end result is that I finally have a NSDictionary object containing arrays with the section ID's as a key. I'd like to reload these sections so we have a kind of sorting.
The problem is, how and where to call the reloadData of the generated tableView? Looks like the tableView itself is not accessible in the Sender method (which sounds kind of logical to me btw) but I do not know how to call the method so the tableView will call it's methods numberOfSections, RowsPerSection etc (pseudocode btw).
Any ideas?
Cheers!
Laurens
-(IBAction)orderBy:(UIBarButtonItem *)sender {
int lv_temp = sender.tag;
choice = lv_temp;
switch (lv_temp) {
case 0:
break;
case 1: {
//This gives us all the different sections available.
typeSections = [Items valueForKeyPath:#"#distinctUnionOfObjects.type"];
//Do some more stuff and create the Dictionary so we hold an array with a key for each section
[ItemsByType2 setObject:ItemsTemp forKey:[NSString stringWithFormat:#"%#", id]];
[ItemsTemp removeAllObjects];
}
[self.tableView reloadData];
}
I'm using TBXML+HTTP to get XML data from a website. What I want to do is populate a UITableView with the processed data.
I've created a NSMutableArray that holds all the entries and, as far as that goes, everything is working fine.
The problem is when, after successfully fetching the data from the server and storing it in the array, I try to update the table to show the data, using reloadData.
It takes about 5 seconds to populate the table with 20 rows. The weird part is that the fetching is happening really fast, so the data is available right away. I do not understand what is taking so long. Here is some info from the log:
2012-03-17 18:46:01.045 MakeMyApp[4571:207] numberOfSectionsInTableView: 1
2012-03-17 18:46:01.047 MakeMyApp[4571:207] numberOfRowsInSection: 0
2012-03-17 18:46:01.244 MakeMyApp[4571:1f03] numberOfSectionsInTableView: 1
2012-03-17 18:46:01.245 MakeMyApp[4571:1f03] numberOfRowsInSection: 20
2012-03-17 18:46:01.245 MakeMyApp[4571:1f03] Ok, I'm done. 20 objects in the array.
2012-03-17 18:46:01.246 MakeMyApp[4571:1f03] Finished XML processing.
2012-03-17 18:46:06.197 MakeMyApp[4571:1f03] cellForRowAtIndexPath:
As you can see, it fires the pair numberOfSectionsInTableView:/numberOfRowsInSection: two times: the first is when the view loads, the second is when I do [self.tableView reloadData];
You see that, in less than a second, the array is populated with the 20 objects and all the work processing the XML is done. How is cellForRowAtIndexPath: fired only 5 seconds later?
Here are some parts of the code that might help finding the problem:
- (void)createRequestFromXMLElement:(TBXMLElement *)element {
// Let's extract all the information of the request
int requestId = [[TBXML valueOfAttributeNamed:#"i" forElement:element] intValue];
TBXMLElement *descriptionElement = [TBXML childElementNamed:#"d" parentElement:element];
NSString *description = [TBXML textForElement:descriptionElement];
TBXMLElement *statusElement = [TBXML childElementNamed:#"s" parentElement:element];
int status = [[TBXML textForElement:statusElement] intValue];
TBXMLElement *votesCountElement = [TBXML childElementNamed:#"v" parentElement:element];
int votesCount = [[TBXML textForElement:votesCountElement] intValue];
// Creating the Request custom object
Request *request = [[Request alloc] init];
request.requestId = requestId;
request.description = description;
request.status = status;
request.votes_count = votesCount;
[requestsArray addObject:request];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
appListCell *cell = (appListCell *)[tableView dequeueReusableCellWithIdentifier:#"appListCell"];
Request *request = [requestsArray objectAtIndex:indexPath.row];
cell.appIdLabel.text = [NSString stringWithFormat:#"#%d", request.requestId];
cell.appTextLabel.text = request.description;
cell.appVotesLabel.text = [NSString stringWithFormat:#"%d votes", request.votes_count];
return cell;
}
Thanks a lot!
EDIT:
- (void)traverseXMLAppRequests:(TBXMLElement *)element {
int requestCount = 0;
TBXMLElement *requestElement = element->firstChild;
// Do we have elements inside <re>?
if ((requestElement = (element->firstChild))) {
while (requestElement) {
[self createRequestFromXMLElement:requestElement];
requestCount++;
requestElement = [TBXML nextSiblingNamed:#"r" searchFromElement:requestElement];
}
}
[self.tableView reloadData];
NSLog(#"Ok, I'm done. %d objects in the array.", [requestsArray count]);
}
EDIT 2: I've tried fetching the information of just 1 row, instead of 20, and the delay is exactly the same.
I know this is a really old question, but I've just come across the exact same issue and think I might know the cause. Might help someone who stumbles across this same question...
I suspect that your reloadData call is happening on a background thread as opposed to the main thread. What you describe is the exact effect of this - the code runs but there is a long delay (5-10 secs) in updating the user interface.
Try changing the reloadData call to:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
You are calling reloadData on a thread other than the mainThread
Make sure to reload your tableView's data on main thread like in the below:
OBJ-C:
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
Swift 3:
DispatchQueue.main.async {
self.tableView.reloadData()
}
Where does:
- (void)createRequestFromXMLElement:(TBXMLElement *)element
get called? Sound like you might be making 20 web requests to get that file contents. That would definitely make things slow (as opposed to doing one request to get the entire XML file and parsing that locally).
Can you post your whole VC code?