I am developing a news application.I am using a table view to show the news. To download data from the server I am using sendAsynchronousRequest .
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
{
downloadedItem = [GNNewsItems saveDataToModel:data];
if ([self.delegate respondsToSelector:#selector(receivedResponse:)])
{
[self.delegate performSelectorOnMainThread:#selector(receivedResponse:) withObject:downloadedItem waitUntilDone:NO];
}
}
else if ([data length] == 0 && error == nil)
{
// Data not downloaded
}
else if (error != nil)
{
// error
}
}];
So far so good. Now consider a case:
User opens the app.
Table View send's a request to download the content of the first cell. Let us assume it takes 10 seconds to download the data.
User scrolls the table view to the 5th cell.
Table view sends the request for 5th cell.
Now user comes back to the first cell but the content of the initial request sent by cell 1 is not yet downloaded.
Table view will send a duplicate request for the first cell.
How can I cancel the duplicate request from the table view?
Create a NSMutableArray which will contain the indexPath of the cell for which request has already been initiated. Before initiating the web request for the a new in a cell check in the if request is already initiated or not. If not then initiate it else do nothing.
Your design description sounds like you are not using a standard Model/View design paradigm. You should store the headlines and other associated data, such as the full story or link to same in some kind of datastore, e.g. an array or CoreData etc. That object is what requests stories and updates data elements. Then it only does it once and in the background, and once gotten, it is done.
Then the tableView uses that datastore to populate table cells. It is a VERY bad design to be making over the air requests for stories each time a cell scrolls into view. It has the design issues you just mentioned plus it is very wasteful of users bandwidth and your server resources.
Related
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.
I have two ViewController and use a (tableview click) seque for opening the second ViewController.
My Problem is, the Second View Controller load much Data. So the time between switch is <> 10 Seconds. In this 10 Seconds the App freeze. Thats OK, but HOW can i insert a "Popup" or "Alert" Message like "Please Wait..." BEVOR . I have testing much tutorials for Popups and Alerts, but the Popup/Alter shows only, when the SecondView Controller is complete loaded. I will show the Message BEVOR the SecondViewController is compled loaded.
Example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// IF i set here the ALERT, the Alter was only show, when the Second View Controller is complete loaded!
NSDictionary *rowVals = (NSDictionary *) [SearchNSMutableArray objectAtIndex:indexPath.row];
[self performSegueWithIdentifier:#"Foo" sender:self];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"Foo"]) {
// Get indexpath from Tableview;
NSIndexPath *indexPath = [self.SearchUITableView indexPathForSelectedRow];
// Get Data from Array;
NSDictionary *rowVals = (NSDictionary *) [self.SearchNSMutableArray objectAtIndex:indexPath.row];
// Destination View;
[MySecondViewController alloc];
MySecondViewController *MyView = (MySecondViewController *)segue.destinationViewController;
}
}
You are trying to fix the problem with the wrong solution. That solution is just as bad because the popup will also freeze for 10 seconds. What if you add more data and it takes 30 seconds or 10 minutes? Are you going to expect your users to see a dialog they can't dismiss for 10 minutes?
Are you fetching the data from the internet? If so you need to fetch your data asynchronously in the background.
If you're loading it from disk then there's too much being loaded that could possibly be displayed on one screen, you need to load only a small portion of it, and if that still takes a long time you need to load it asynchronously.
UPDATED -
You should have a model class for your application that is responsible for fetching the data from the internet.
Google Model View Controller to get some background information on what a Model is.
As soon as the app launches the model can start to download the data which needs to be down in the background (that's too big a topic to answer how to do that here).
The View controller can launch while the data is being downloaded and it can display a spinning activity indicator wheel or progress bar or dialog etc. while waiting. The important thing is the GUI will not freeze.
When the model has downloaded the data it needs to tell the view controller the data is now available, which it can do using NSNotification center.
There's lots for you to investigate and learn, to do it without GUI freezing it needs to be done properly, there's no shortcut, you have a lot to study.
#Martin,
i found a solution:
// Send the Request;
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
So the request are asynchrony. Thanks for your answer. Great +1
I went from using an NSURLConnection in my tableview controller to using an NSURLSession in a separate class with a callback that is processed in the tableview controller.
Now the data returned from my website does not get displayed in the populated table for up to 20 seconds even though it was loaded long ago.
I have an 'add' button on the navigation bar of tableview which brings up another view. When I click the add button, I can see the data in my main tableview is already populated as it animates to the next view. Returning back to the main view and the data is there.
I have tried implementing a number of ways to reload the data but they have no effect.
The old way which works fine, only lets me have one connection. I needed to have several connections available to call based on options I might have selected which is the reason for creating a new class to handle the connections and placing its callback in the tableview.
But this has created the problem of not being able to view the parsed return data immediately.
To me, it seems to be some type of threading issue, but I don't know how to troubleshoot it or how to correct it, so I am hoping someone here can suggest something to try.
Here is the applicable code in my new class we will call NetWorkClass for purposes of illustration...
#pragma mark - Get Parents
+ (void)requestParentsWithCompletionHandler:(RequestCompletionHandler)completionBlock {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"insert url here"]];
[request setHTTPMethod:#"POST"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig];
[[session dataTaskWithRequest:request completionHandler:completionBlock] resume];
}
This typdef is in the header file for my new class NetworkClass...
typedef void (^RequestCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error);
Then in the main thread of my tableview controller class I call a method that invokes the Network class to retrieve JSON data from a website.
The following snippet is called from ViewDidLoad in my tableview controller...
// Use NSSesssion to request JSON data from my website [self getParents];
NSLog(#"getParents has completed!");
// The view however, will not display for approximately 20 or 30 seconds unless I click on the Add button which instantiates another view // and then I see the data in the tableview right away as it animates to the new view. NSLog(#"parentsTable:%#",parentsTable);
NSLog outputs the following:
2014-02-11 16:10:27.385 myApp[12667:70b] parentsTable:; layer = ; contentOffset: {0, 0}>
The data has not yet been processed by the callback at this point, but the website has been sent the request and the repsonse is being returned.
The callback is implemented in getParents...
-(void)getParents
{
[NetWorkClass requestParentsWithCompletionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if (!error) {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
if the response status code is 200, I propagate the data into the tableview and finish up by calling
[parentsTable reloadData];
All of the code following the statuscode check is identical to the code that works if I use an NSURLconnection (and its delegate methods) within the tableview to retrieve the data, so I have not included it here.
If I put a breakpoint at requestParentsWithCompletionHandler in getParents, and I single step from there, the first pass skips around my code. But if I then run from that point, it hits the breakpoint a second time and then falls thru to process the response, which is working as I would expect it since we have to wait a few milliseconds for the data to be obtained from the website.
The data has arrived within milliseconds, but it can take up to 20 seconds before the tableview fills in with data that was parsed if I do nothing.
Any help with this would be greatly appreciated.
You need to change your structure to the following:
In viewDidLoad You should call a method that will send an asynchronous request for the data. (strongly suggest using AFNetworking or restKit to do it instead of trying to get by with the code you are currently using)
On success you should populate an array with the data and [tableView reloadData]
You numberOfRowsInSection should look like the following:
If (!arrayContainingData) {
return 0; // For elegance, you can also return 1 and have a cell with an UIActivityMonitor
} else {
return arrayContainingData.count;
}
Finally use cellForRowAtIndexPath to build the cells.
I have a weird issue going on with my UITableView. Even though I have specified a height for it in IB, when it is reloaded, sometimes the height changes -- usually increasing. This causes the bottom of the table to be obscured by other view elements, and makes it impossible to scroll to very bottom row.
Why does the table size change by itself?
There is a products view, where the user selects a bunch of products to add to their order. They then tap a button to review their order. the product view passes the products information to the next view, lets call it the cart view and loads it. This is the cart view:
The View:
The cart view loads the products into the table, it then submits a request to the server to validate the order. The server responds with errors (like not enough inventory available for a product) and any applicable discounts. The table is then reloaded with this information included (the errors appear inside the cells and the discount items are added as new cells).
Relevant Code From the Cart View Controller:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (!self.initialized) {
[self saveOrderOnFirstLoad];
}
}
- (void)saveOrderOnFirstLoad {
if ([helper isOrderReadyForSubmission:self.coreDataOrder]) {
self.coreDataOrder.status = #"pending";
[[CoreDataUtil sharedManager] saveObjects];
NSDictionary *parameters = [self.coreDataOrder asJSONReqParameter];
//todo should we keep completed orders, complete? Or should we update status to pending if they go to cart view, even if they did not make any changes?
NSString *method = [self.coreDataOrder.orderId intValue] > 0 ? #"PUT" : #"POST";
NSString *url = [self.coreDataOrder.orderId intValue] == 0 ? [NSString stringWithFormat:#"%#?%#=%#", kDBORDER, kAuthToken, self.authToken] : [NSString stringWithFormat:#"%#?%#=%#", [NSString stringWithFormat:kDBORDEREDITS([self.coreDataOrder.orderId intValue])], kAuthToken, self.authToken];
void (^successBlock)(NSURLRequest *, NSHTTPURLResponse *, id) = ^(NSURLRequest *req, NSHTTPURLResponse *response, id json) {
self.savedOrder = [self loadJson:json];
self.unsavedChangesPresent = NO;
};
void(^failureBlock)(NSURLRequest *, NSHTTPURLResponse *, NSError *, id) = ^(NSURLRequest *req, NSHTTPURLResponse *response, NSError *error, id json) {
if (json) {
[self loadJson:json];
}
};
[helper sendRequest:method url:url parameters:parameters successBlock:successBlock failureBlock:failureBlock view:self.view loadingText:#"Submitting order"];
}
self.initialized = YES;
}
- (AnOrder *)loadJson:(id)json {
AnOrder *anOrder = [[AnOrder alloc] initWithJSONFromServer:(NSDictionary *) json];
[self.managedObjectContext deleteObject:self.coreDataOrder];//delete existing core data representation
self.coreDataOrder = [helper createCoreDataCopyOfOrder:anOrder customer:self.customer loggedInVendorId:self.loggedInVendorId loggedInVendorGroupId:self.loggedInVendorGroupId managedObjectContext:self.managedObjectContext];//create fresh new core data representation
[[CoreDataUtil sharedManager] saveObjects];
[self refreshView];
return anOrder;
}
- (void)refreshView {
self.productsInCart = [helper sortProductsByinvtId:[self.coreDataOrder productIds]];
self.discountsInCart = [helper sortDiscountsByLineItemId:[self.coreDataOrder discountLineItemIds]];
[self.productsUITableView reloadData];
[self updateTotals];
}
The height of the table set in IB is 459.
When I debug, I find that when the table is loaded the first time, the debugger shows the height of the table to be 459. But when it is loaded again (after response is received from the server, the height is no longer 459):
And this seems to happen only when there is a large number of cells in the table.
[UPDATE]
The issue turned out to be that I had autolayout turned on on the table while it was turned off on its parent view. Once I turned autolayout on for the parent view, the height of the table stayed constant and I could scroll to the bottom row.
I think the problem is on your nib AutoLayout is enabled. That's why it is not accepting your height. Once you use auto layout all of the modification in the subviews will be ignored. If you are going with non-autolayouts you should go for all of your views.
My app is getting a feed of JSON data quite big representing thousands of products data. After parsing and saving data in CoreData (all in background threads), displaying data in UITableView after parsing block the UI since it's done in the main thread. But the problem is that the UI remains blocked for few seconds which is not user friendly. So how do you suggest I can handle reloading data without blocking UI?
//...
//After parsing data, reload UITableView
[self.tView reloadData];
EDIT:
I may re-explain my issue, after parsing data, reloading data, UITableView object display all data. Then UI is blocked for few seconds before I can finally use the app again.
here is my relevant code:
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON){
//Parse data
[self.tView reloadData];//Display data
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
//Save data
}];
}failure:^(NSURLRequest *request, NSHTTPURLResponse *response,NSError *error, id JSON){
//
}];
[operation start];
}
Based on sangony comment, it turns out that the call of tableView:heightForRowAtIndexPath: delegate method affect performance especially with my case where I deal with over than 3000 rows. here is Apple documentation related to that:
There are performance implications to using tableView:heightForRowAtIndexPath: instead of
the rowHeight property. Every time a table view is displayed, it calls
tableView:heightForRowAtIndexPath: on the delegate for each of its rows, which can result in a
significant performance problem with table views having a large number of rows (approximately
1000 or more).
From other threads discussing similar issues, I can get rid of performance issues by assigning the row height on the table view property:
self.tView.rowHeight = 200.0;
And of course, remove the implementation of tableView:heightForRowAtIndexPath: otherwise it will override the rowHeight property. Note that this solve my problem only in case I deal with single row height. So this cannot help in case where I need to apply more than one row height value, where tableView:heightForRowAtIndexPath: is required.
Use NSThread to call a method in which you fetch Json data and after fetching, reload the table in the same method.
[NSThread detachNewThreadSelector:#selector(GetJsonDataAndReload) toTarget:self withObject:nil];
So,this process will not block user interaction.