Seems like my app is doing stuff in the wrong order - ios

(Note: You can probably even answer this without reading any of my code, if you don't feel like reading this novel of a question, I've explained at the bottom)
I have some code that's throwing me a bit off course. It seems like my code is calling [tableView reloadData] before the data is ready, although I set the data before I call reloadData. This is probably a newbie-question, but I have never dealt with this kind of code before, and I'd be very happy if someone could explain to me exactly what is going on, and why some of my code is allowed to jump out of the chronological logic..
My question has nothing to do with GameCenter, I think this is a general Objective-C thing, but I am experiencing it with GameCenter, so even if you don't know anything about GameCenter, you'll probably be able to understand what I'm talking about.
I have a tableViewController, and I am filling the tableView with matches(games) from GameCenter, and showing the names of the opponent(always one opponent in my turn-based-game).
I have put this in my viewDidAppear:
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error) {
//Gets an array (*matches) of GKTurnBasedMatches(All matches the local player is involved in)
if(error)
{
NSLog(#"Load matches error: %#", error.description);
return;
}
//putting them in an array
matchList = [[NSArray alloc] initWithArray:matches];
//So far only for testing purposes: I am creating an array for player names:
playerNames = [[NSMutableArray alloc] init];
for(GKTurnBasedMatch* mat in matchList)
{//For every match in the array of matches, find the opponent:
GKTurnBasedParticipant *otherParticipant; //These games only have 2 players. Always.
for(GKTurnBasedParticipant *part in [mat participants])
{ //Loop through both participants in each match
if(NO == [part.playerID isEqualToString:[GKLocalPlayer localPlayer].playerID])
{//If the participant *part is NOT the local player, then he must be the opponent:
otherParticipant = part; //Save the opponent.
}
}
if(otherParticipant != nil && otherParticipant.playerID != nil)
{ //If the opponent exists(like he does)
[GKPlayer loadPlayersForIdentifiers:#[otherParticipant.playerID] withCompletionHandler:^(NSArray *players, NSError *error) {
//Returns an array with only the opponent in it
if([(GKPlayer*)[players objectAtIndex:0] displayName]!=nil)
{//If he has a name:
NSString *nam = [[NSString alloc] initWithString:[(GKPlayer*)[players objectAtIndex:0] displayName]];
//Add the name to the name-array
[playerNames addObject:nam];
}
else
{//If he doesn't have a name, put "No name" into the name array(is not happening)
NSString *nam = [[NSString alloc]initWithString:#"No name"];
[playerNames addObject:nam];
}
}];
}
else
{//If for some reason the entire opponent is nil or he doesn't have a playerID, which isn't happening, add "No name";(This is not happening)
[playerNames addObject:[NSString stringWithFormat:#"No name"]];
}
}
//And THEN reload the data, now with the NSMutableArray 'playerNames' ready and filled with opponent's names.
[[self tableView] reloadData];
}];
This is my CellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"matchCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *name = #"Nobody";
if(playerNames!=nil && [playerNames count]>indexPath.row)
name = [playerNames objectAtIndex:indexPath.row];
[[cell textLabel] setText:[NSString stringWithFormat:#"Game vs. %#", name]];
return cell;
}
The thing is: when the view controller appears: as expected, the tableView is initially empty, but after about half a second, it fills with "Game vs. Nobody" in the correct number of games. If I start two games, and re-enter the view controller, then it's blank at first, and then show "Game vs. Nobody" on two cells. BUT: if I scroll the rows outside the screen, and force them to call for cellForRowAtIndexPath again, then the correct name shows up. So it seems like the [tableView reloadData] in viewDidAppear is called before I ask it to, before the array with names has been filled.
I assume this has something to do with the whole CompletionHandler-thing, as I have never used this before, but I thought everything inside the CompletionHandler was supposed to happen chronologically? Is there something I can call, like onCompletion{} or something?
Ideally, I want an UIAlertView or something to show up with a spinning wheel, the UIActivityIndicationView, and I want it to show until all the data has been loaded correctly. I don't know how to find out WHEN the completionHandler is done, and what it really is..

The playerNames array is being filled within the completion handler for loadPlayersForIdentifiers, which is (I assume) asynchronous. The names array is not populated until the completion handler runs, but you are calling [tableView reloadData] within the other block.
To fix, just move the reload call within the completion block for loadPlayersForIdentifiers (at the end)

Related

App Not Sending PNGs

Trying to learn by digging through some code.
Have an accessory button, when its pressed it loads up a menu so the user can pick either a picture from photo library or video (works perfect) have also a sticker menu that pops up similarly to the photo library. The sticker pops up accordingly, only have one if that matters. However when I select it nothing happens. The view is dismissed and it should be sent however it is not.v THE NSLog prints my check statement, I have even added the JSQ sound to play and it does it as well. Working with Firebase/Parse.
It seems to call everything just doesn't attach the PNG from the grid view to the message.
StickerView.m
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
NSString *file = stickers2[indexPath.item];
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
}
Chat.m
- (void)didSelectSticker:(NSString *)sticker
{
[self messageSend:sticker Video:nil Picture:nil Audio:nil];
}
messageSend
- (void)messageSend:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio
{
Outgoing *outgoing = [[Outgoing alloc] initWith:groupId View:self.navigationController.view];
[outgoing send:text Video:video Picture:picture Audio:audio Sticker:sticker];
[JSQSystemSoundPlayer jsq_playMessageSentSound];
[self finishSendingMessage];
}
LOG
NSString *file = stickers2[indexPath.item];
NSLog (#"%d",indexPath.item);
NSLog (#"%#",file);
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#"#2x.png"];
NSLog (#"%#",sticker);
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
indexPath returns a numerical value based on which sticker is selected. These are loaded into the mutableArray based on file name. If the file name contains a string of #"foo" it is loaded into the array. it could be one or one million value based on how many are loaded in. However it does correspond correctly to the selections. One is properly returned when object one is selected and 2 is properly selected and so on.
File name is returned as the correct file name.
Sticker was returned incorrectly based on your suspicious line of code, I have replaced it to not removed the extensions but did not delete it as of yet. Now returning the same value as File.
Outgoing
- (void)send:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio Sticker:(NSString *)sticker
{
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];
item[#"userId"] = [PFUser currentId];
item[#"name"] = [PFUser currentName];
item[#"date"] = Date2String([NSDate date]);
item[#"status"] = TEXT_DELIVERED;
item[#"video"] = item[#"thumbnail"] = item[#"picture"] = item[#"audio"] = item[#"latitude"] = item[#"longitude"] = #"";
item[#"video_duration"] = item[#"audio_duration"] = #0;
item[#"picture_width"] = item[#"picture_height"] = #0;
if (text != nil) [self sendTextMessage:item Text:text];
else if (video != nil) [self sendVideoMessage:item Video:video];
else if (picture != nil) [self sendPictureMessage:item Picture:picture];
else if (audio != nil) [self sendAudioMessage:item Audio:audio];
else if (sticker !=nil) [self sendSticker:item Sticker:sticker];
else [self sendLoactionMessage:item];
}
sendSticker
- (void)sendSticker:(NSMutableDictionary *)item Sticker:(NSString *)sticker
{
item[#"sticker"] = sticker;
NSLog(#"%#",sticker);
[self sendMessage:sticker]
}
NSLog never gets called so I know that this is not getting called. I must be missing something, after the user selects the sticker.
Sticker.h
#protocol StickersDelegate
- (void)didSelectSticker:(NSString *)sticker;
#end
#interface StickersView : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate>
#property (nonatomic, assign) IBOutlet id<StickersDelegate>delegate;
#end
I see a couple of potential problem areas, but nothing definitive. I'd check the following either using a debugger or by adding additional logging:
What's the actual numerical value of indexPath.item? Does it represent a "valid" index into your array stickers2?
What is the value of file after you look it up? Is it a valid string, and does it correctly represent the name of an actual file on your file system?
What does the Outgoing class expect for the value of text in its -send:Video:Picture:Audio: method, a fully-qualified path name, or just a simple file name with no path? If the former, you'll either need to look up the system directory and construct the full file path before passing it in, or it would have needed to be constructed that way originally and inserted as such into stickers2, and if the latter, I'm assuming you will have needed to provided a "base" directory to Firebase/Parse somewhere.
This line is suspicious:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
Do you mean to strip off the extension entirely? In other words, if you have a base filename of "foo#2x.png", then this will change "foo#2x.png" to "foo", is that what you really want. I'm guessing you might want this to be "foo.png" instead:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#".png"];
?
Hope this helps you track it down.

How to make API call in UITableVIewCell

I have a Scenario in which I have to make a API request to update UILables in TableViewCell .
The problem is that for each cell I have to make a Unique API request. The API url is same but the parameter is different.
Currently I am making calls in cellForRowAtIndex and In success block I am using dispatch_async to update the array and reloading the UITableView.
My cellForRowAtIndexMethod :
if(!apiResponded) //Bool value to check API hasn't responded I have to make API request
{
cell.authorLabel.text = #"-------";// Set Nil
NSString *userId =[CacheHandler getUserId];
[self.handleAPI getAuthorList:userId]; //make API Call
}
else
{
cell.authorLabel.text = [authorArray objectAtIndex:indexPath.row];// authorArray is global Array
}
My success Block of API Request :
numOfCallsMade = numOfCallsMade+1; //To track how manny calls made
apiResponded = YES; // to check API is reponded and I have to update the UILables
dispatch_async(kBgQueue, ^{
if(!authorArray)
authorArray = [[NSMutableArray alloc]init];
NSArray *obj = [responseData valueForKey:#"aName"];
if(obj == nil)
{
[authorArray addObject:#"N/A"];
}
else
{
[authorArray addObject:[obj valueForKey:#"authorName"]];
}
dispatch_async(dispatch_get_main_queue(), ^{
if(numOfCallsMade == [self.mCarsArray count]) // this is to check if I have 10 rows the 10 API request is made then only update
[self.mTableView reloadData];
});
});
When I run this code I am getting Same Value for each Label. I don't know my approach is good or not. Please any one suggest how can Achieve this.
From your code, I’m not really sure what you want to achieve. All I know is that you want to make a request per each cell, and display received data. Now I don’t know how you’d like to store your data, or how you’ve setup things, but I’ll give you a simple suggestion of how you could set this up, and then you can modify as needed.
I assume you only need to make this request once per cell. For simplicity, we could therefore store a dictionary for the received data (author names?).
#property (nonatomic, strong) NSMutableDictionary *authorNames;
We need to instantiate it before usage, inside init or ViewDidLoad, or wherever you see fit (as long as it's before TableView calls cellForRowAtIndexPath:).
authorNames = [[NSMutableDictionary alloc] init];
Now in cellForRowAtIndexPath, you could do the following:
NSInteger index = indexPath.row
cell.authorLabel.text = nil;
cell.tag = index
NSString *authorName = authorNames[#(index)];
if (authorName) { // Check if name has already exists
cell.authorLabel.text = authorName;
} else {
// Make request here
}
In your requests completion block (inside CellForRowAtIndexPath:), you add this:
NSString *authorName = [responseData valueForKey:#“aName”];
authorNames[#(index)] = authorName; // Set the name for that index
if (cell.index == index) { // If the cell is still being used for the same index
cell.authorLabel.text = authorName;
}
When you scroll up and down in a TableView, it will reuse cell that are scrolled outside of the screen. That means that when a request has finished, the cell could have been scrolled offscreen and reused for another index. Therefore, you want to set the cell tag, and when the request has completed, check if the cell is still being used for the index you made the request for.
Potential issues: When scrolling up and down fast, when your requests are still loading, it could potentially make multiple requests for each cell. You'll have to add some way to just make each request once.
You can declare a method in your custom cell and then call it from cellForRowAtIndex , the method will call the API and update the label present only in that cell.
So for each cell you will have separate method calls & each success block will update the particular cell Label text only.

UICollectionView reloadData seems slow/laggy (not instantaneous)

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.

Multiple NSUrlRequests To Fetch Different Pages Of WebService To Load More Data In TableView

I have a simple iPhone app that is parsing data (titles, images etc.) from rss feed and showing in the tableview.
The viewDidLoad has an initial counter value to reach the first page of the feed and load in the tableview by calling the fetchEntriesNew method:
- (void)viewDidLoad
{
[super viewDidLoad];
counter = 1;
[self fetchEntriesNew:counter];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(dataSaved:)
name:#"DataSaved" object:nil];
}
- (void) fetchEntriesNew:(NSInteger )pageNumber
{
channel = [[TheFeedStore sharedStore] fetchWebService:pageNumber withCompletion:^(RSSChannel *obj, NSError *err){
if (!err) {
int currentItemCount = [[channel items] count];
channel = obj;
int newItemCount = [[channel items] count];
NSLog(#"Total Number Of Entries Are: %d", newItemCount);
counter = (newItemCount / 10) + 1;
NSLog(#"New Counter Should Be %d", counter);
int itemDelta = newItemCount - currentItemCount;
if (itemDelta > 0) {
NSMutableArray *rows = [NSMutableArray array];
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[[self tableView] insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[aiView stopAnimating];
}
}
}];
[[self tableView] reloadData];
}
When the user reaches the bottom of the tableview, i am using the following to reach the next page of the feed and load at the bottom of the first page that was loaded first:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
UPDATE2: Here is a more easy to understand description of whats wrong that i am unable to solve: For example there are 10 entries in the each page of the rss feed. The app starts, titles and other labels are loaded immediately and images starts loading lazily and finally gets finished. So far so good. The user scrolls to reach the bottom, reaching the bottom will use the scroll delegate method and the counter gets incremented from 1 to 2 telling the fetchEntriesNew method to reach the second page of the rss feed. The program will start loading the next 10 entries at the bottom of first 10 previously fetched. This can go on and the program will fetch 10 more entries every time the user scrolls and reaches bottom and the new rows will be placed below the previously fetched ones. So far so good.
Now let us say the user is on page 3 currently which has been loaded completely with the images. Since page 3 is loaded completely that means currently there are 30 entries in the tableview. The user now scrolls to the bottom, the counter gets incremented and the tableview begins populating the new rows from page 4 of the rss feed at the bottom of the first 30 entries. Titles get populated quickly thus building the rows and while the images are getting downloaded (not downloaded completely yet), the user quickly moves to the bottom again, instead of loading the 5th page at the bottom of the 4th, it will destroy the 4th ones that is currently in the middle of getting downloaded and starts loading the 4th one again.
What it should do is that it should keep on titles etc from next pages when user reaches the bottom of the tableview regardless of whether the images of the previous pages are in the middle of getting downloaded or not.
There is NO issue with the downloading and persisting data in my project and all the data is persisted between the application runs.
Can someone help to point me out to the right direction. Thanks in advance.
UPDATE 3: Based on #Sergio's answer, this is what i did:
1) Added another call to archiveRootObject [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath]; after [channelCopy addItemsFromChannel:obj];
At this point, its not destroying and reloading the same batch again and again, exactly what i wanted. However, it doesn't persist images if i scroll multiple times to reach the next page without the images of the previous page were loaded completely.
2) I am not sure how to use Bool as he explained in the answer. This is what i did: Added #property Bool myBool; in TheFeedStore, synthesised it and set it to NO after newly added archiveRootObject:channelCopy and set it to YES in ListViewController at the very start of fetchEntries method. It didn't work.
3) I also realised the way i am dealing with the whole issue is performance vice not better. Although i don't know how to use images outside the cache and handle them as sort of cache. Are you suggesting to use a separate archiving file for images?
Thanks a lot to all people who have contributed in trying to solve my issue.
Your issue can be understood if you consider this older question of yours and the solution I proposed.
Specifically, the critical bit has to do with the way you are persisting the information (RSS info + images), which is through archiving your whole channel to a file on disk:
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];
Now, if you look at fetchEntriesNew:, the first thing that you do there is destroying your current channel. If this happens before the channel has been persisted to disk you enter a sort of endless loop.
I understand you are currently persisting your channel (as per my original suggestion) at the very end of image download.
What you should do is persisting the channel just after the feed has been read and before starting downloading the images (you should of course also persist it at the end of image downloads).
So, if you take this snippet from my old gist:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
what you should do is adding one more archiveRootObject call:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
This will make things work as long as you do not scroll fast enough so that the channel is destroyed before the feed (without images) is ever read. To fix this you should add a bool to your TheFeedStore class that you set to YES when you call fetchWebService and reset just after executing the newly added archiveRootObject:channelCopy.
This will fix your issues.
Let me also say that from a design/architecture point of view, you have a big issue with the way you manage persistence. Indeed, you have a single file on disk that you write atomically using archiveRootObject. This architecture is intrinsically "risky" from a multi-threading point of view and you should also devise a way to avoid that concurrent accesses to the shared stored have no destructive effects (e.g.: you archive your channel to disk for page 4 at the same time as the images for page 1 have been fully downloaded, hence you try to persist them as well to the same file).
Another approach to image handling would be storing the images outside of your archive file and treat them as a sort of cache. This would fix the concurrency issues and will also get rid of the performance penalty you get from archiving the channel twice for each page (when the feed is first read and later when the images have come in).
Hope this helps.
UPDATE:
At this point, its not destroying and reloading the same batch again and again, exactly what i wanted. However, it doesn't persist images if i scroll multiple times to reach the next page without the images of the previous page were loaded completely.
This is exactly what I meant saying that your architecture (shared archive/concurrent access) would probably lead to problems.
You have several options: use Core Data/sqlite; or, more easily, store each image in its own file. In the latter case, you could do following:
on retrieval, assign to each image a filename (this could be the id of the feed entry or a sequential number or whatever) and store the image data there;
store in the archive both the URL of the image and the filename where it should be stored;
when you need accessing the image, you don't get it from the archived dictionary directly; instead, you get the filename from the it then read the file from disk (if available);
this change would not affect otherwise your current implementation of rss/image retrieval, but only the way you persist the images and you access them when required (I mean, it seems a pretty easy change).
2) I am not sure how to use Bool as he explained in the answer.
add a isDownloading bool to TheFeedStore;
set it to YES in your fetchWebService: method, just before doing [connection start];
set it to NO in the completion block you pass to the connection object (again in fetchWebService:) right after archiving the feed the first time (this you are already doing);
in your scrollViewDidEndDecelerating:, at the very beginning, do:
if ([TheFeedStore sharedStore].isDownloading)
return;
so that you do not refresh the rss feed while a refresh is ongoing.
Let me know if this helps.
NEW UPDATE:
Let me sketch how you could deal with storing images in files.
In your RSSItem class, define:
#property (nonatomic, readonly) UIImage *thumbnail;
#property (nonatomic, strong) NSString *thumbFile;
thumbFile is the the path to the local file hosting the image. Once you have got the image URL (getFirstImageUrl), you can get, e.g., and MD5 hash of it and use this as your local image filename:
NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];
(MD5String is a category you can google for).
Then, in downloadThumbnails, you would store the image file locally:
NSMutableData *tempData = [NSData dataWithContentsOfURL:finalUrl];
[tempData writeToFile:[self cachedFileURLFromFileName:self.thumbFile] atomically:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:#"DataSaved" object:nil];
Now, the trick is, when you access the thumbnail property, you read the image from file and return it:
- (UIImage *)thumbnail
{
NSData* d = [NSData dataWithContentsOfURL:[self cachedFileURLFromFileName:self.thumbFile]];
return [[UIImage alloc] initWithData:d];
}
in this snippet, cachedFileURLFromFileName: is defined as:
- (NSURL*)cachedFileURLFromFileName:(NSString*)filename {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray *fileArray = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask];
NSURL* cacheURL = (NSURL*)[fileArray lastObject];
if(cacheURL)
{
return [cacheURL URLByAppendingPathComponent:filename];
}
return nil;
}
Of course, thumbFile should be persisted for this to work.
As you see, this approach is pretty "easy" to implement. This is not an optimized solution, just a quick way to make your app work with its current architecture.
For completeness, the MD5String category:
#interface NSString (MD5)
- (NSString *)MD5String;
#end
#implementation NSString (MD5)
- (NSString *)MD5String {
const char *cstr = [self UTF8String];
unsigned char result[16];
CC_MD5(cstr, strlen(cstr), result);
return [NSString stringWithFormat:
#"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
#end
What you are actually trying to do, is implement paging in a UITableView
Now this is very straightforward and the best idea is to implement the paging in your UITableView delegate cellForRowAtIndexPath method, instead of doing this on the UIScrollView scrollViewDidEndDecelerating delegate method.
Here is my implementation of paging and I believe it should work perfectly for you too:
First of all, I have an implementation constants related to the paging:
//paging step size (how many items we get each time)
#define kPageStep 30
//auto paging offset (this means when we reach offset autopaging kicks in, i.e. 10 items before the end of list)
#define kPageBegin 10
The reason I'm doing this is to easily change the paging parameters on my .m file.
Here is how I do paging:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=currentItems && !isLoadingNewItems && currentItems+1<maxItems) {
//begin request
[self LoadMoreItems];
}
......
}
currentItems is an integer that has the number of the tableView datasource current items.
isLoadingNewItems is a boolean that marks if items are being fetched at this moment, so we don't instantiate another request while we are loading the next batch from the server.
maxItems is an integer that indicates when to stop paging, and is an value that I retrieve from our server and set it on my initial request.
You can omit the maxItems check if you don't want to have a limit.
and in my paging loading code I set the isLoadingNewItems flag to true and set it back to false after I retrieve the data from the server.
So in your situation this would look like:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=counter && !isDowloading) {
//begin request
isDowloading = YES;
[self fetchEntriesNew:counter];
}
......
}
Also there is no need to reload your whole table after adding the new rows.
Just use this:
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
A simple BOOL is enough to avoid repetitive calls:
BOOL isDowloading;
When the download is done, set it to NO. When it enters here:
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
put it to YES. Also don't forget to set it to NO when the requests fails.
Edit 1:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
if(!isDowloading)
{
isDownloading = YES;
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
}
And when you finish fetching, just set it to NO again.

UITableView reloadData taking too much time

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?

Resources