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.
Related
I am using AFNetworking to download files from my server. It works fine. But I have one issue: My ProgressView updates wrong cell(UI, not data) when I scroll up or down. Here is my code:
My Cell:
AFHTTPRequestOperation *operation;
#property (weak, nonatomic) IBOutlet DACircularProgressView *daProgressView;
- (IBAction)pressDown:(id)sender {
AFAPIEngineer *apiEngineer = [[AFAPIEngineer alloc] initWithBaseURL:[NSURL URLWithString:AF_API_HOST]];
operation = [apiEngineer downloadFile:(CustomObject*)object withCompleteBlock:^(id result) {
} errorBlock:^(NSError *error) {
}];
__weak typeof(self) weakSelf = self;
apiEngineer.afProgressBlock = ^(double progress, double byteRead, double totalByToRead) {
[weakSelf.daProgressView setProgress:progress animated:YES];
};
}
- (void)setDataForCell:(id)object{
}
My table:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomCell *cell = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:NSStringFromClass([CustomCell class])];
cell.backgroundColor = [UIColor clearColor];
CustomObject *aObject = [listObject objectAtIndex:indexPath.row];
[cell setDataForCell: aObject];
return cell;
}
My downloadHelper:
- (AFDownloadRequestOperation*)downloadFile:(CustomObject*)aObject
withCompleteBlock:(AFResultCompleteBlock)completeBlock
errorBlock:(AFResultErrorBlock)errorBlock{
NSString *link = URL;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:link]];
NSString *path = [NSString databasePathWithPathComponent:[NSString stringWithFormat:#"%#.zip", #"noname"]];
AFDownloadRequestOperation *operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:path shouldResume:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completeBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
errorBlock(error);
}];
[operation setProgressiveDownloadProgressBlock:^(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
float progressF = (float)totalBytesReadForFile / totalBytesExpectedToReadForFile;
self.afProgressBlock(progressF, totalBytesReadForFile, totalBytesExpectedToReadForFile);
}];
[operation start];
return operation;
}
When I press button 'Download' in first cell:
When I scroll down then scroll up, It is second cell:
So, my question is: How to update UIProgressView on UITableViewCell? What is wrong with my code?
Because cells are re-used, you simply can't just keep a weak reference to the cell and update the cell's progress view. At the very least, rather than using a weak reference to the table view cell, you would use the index path, look up the correct cell using [tableView cellForRowAtIndexPath:indexPath] (this is a UITableView method that identifies the cell associated with a particular NSIndexPath and should not to be confused with the similarly named UITableViewDataSource method in which we do our cell dequeueing/configuring logic), and update that cell's progress view (assuming that cell is even visible).
Frankly, even that is dangerous, as it assumes that it's impossible to add or remove cells while downloads are in progress. (And that is obviously not a reasonable assumption/constraint to place upon an app.) Quite frankly, every time one wants to update download progress, one really should go back to the model, lookup the correct NSIndexPath based for the model object in question, and then use the above cellForRowAtIndexPath pattern to find and then update the cell in question.
Note, that suggests that the initiation of the download request may not be initiated by the table view cell. Personally, I maintain a model object that consists of an array of files to download, and associate the download queue with that, rather than the cell (though the progress handler would obviously update the cell). In short, you want a loosely coupled relationship between the UI (e.g. the cell) and the model driving the downloads.
Note, many naive implementations consider all of the above points and decide to abandon cell reuse in order to simplify the problem. I won't belabor the point, but I think that's a fundamentally misguided approach. In my opinion, one should have a proper model for the downloads separate from the view objects.
I have a small project on github that basically does what you want with a different style progress indicator. I built it a while back and it uses exactly the approach Rob mentions: The table and its cells are backed by a "model" that tells cells at a given index what their state should be. I also make sure cells are reused. It may be of some use to you:
https://github.com/chefnobody/StreamingDownloadTest
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.
Can anybody explain to me how MVC works when it comes to UITableView especially when getting data from the internet.
I would exactly like to know what is the model, view and controller when it comes to a UItableview
I have written the following ViewController code which sources data from the internet and displays it on a table using AFNetworking framework.
Could you please tell me how to change this and separate it into model, view and controller.
I have also written a refresh class, which i am guessing is a part of the model. Could you tell me how exactly do i make changes and make it a part of the model.
EDIT : The below answers help me understand the concept theoritically, Could someone please help me in changing the code accordingly( By writing a new class on how to call the array to this class and populate the table because i am using a json parser). I would like to implent it. And not just understand it theoritically.
#import "ViewController.h"
#import "AFNetworking.h"
#implementation ViewController
#synthesize tableView = _tableView, activityIndicatorView = _activityIndicatorView, movies = _movies;
- (void)viewDidLoad {
[super viewDidLoad];
// Setting Up Table View
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, self.view.bounds.size.height) style:UITableViewStylePlain];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.hidden = YES;
[self.view addSubview:self.tableView];
// Setting Up Activity Indicator View
self.activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicatorView.hidesWhenStopped = YES;
self.activityIndicatorView.center = self.view.center;
[self.view addSubview:self.activityIndicatorView];
[self.activityIndicatorView startAnimating];
// Initializing Data Source
self.movies = [[NSArray alloc] init];
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rocky&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
[refreshControl endRefreshing];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
}
- (void)refresh:(UIRefreshControl *)sender
{
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rambo&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
[sender endRefreshing];
}
- (void)viewDidUnload {
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
// Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.movies && self.movies.count) {
return self.movies.count;
} else {
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = #"Cell Identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellID];
}
NSDictionary *movie = [self.movies objectAtIndex:indexPath.row];
cell.textLabel.text = [movie objectForKey:#"trackName"];
cell.detailTextLabel.text = [movie objectForKey:#"artistName"];
NSURL *url = [[NSURL alloc] initWithString:[movie objectForKey:#"artworkUrl100"]];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"placeholder"]];
return cell;
}
#end
It's a pretty big question you are asking. But let me answer by making it as simple as possible.
Model - your data source; ultimately it's your web service data
Controller should be the thing that owns the table view and mediates setting properties on your view and reacting to events in the view and making changes , as needed, to the model
View(s) -- a combination of your table view and table view cells
There are a lot of approaches to coordinating between your web data and your table view but one I might suggest would be to refactor your web service calls into a separate store class - say iTunesStore - have that class be responsible for making the calls to the service and setting an internal array with the results, it should also be able to return a row count as well as a specific item for a given row index.
You then have this class respond to calls for the required table view delegate methods. Other things to consider, make this other class a singleton, have it conform to UITableviewDatasource protocol itself and assign it as the table views' data source.
Like I said, a big question with a lot of options for you, but I've given you some things to consider in terms of where to go next.
UPDATE
I'm adding some code examples to help clarify. At the outset, I want to make clear that I am not going to provide the total solution because doing so would require me to assume too much in terms of the necessary actual solution -- and because there are a few different ways to work with AFNetworking, web services, etc....and I don't want to get side tracked going down that rabbit hole. (Such as caching data on the client, background tasks & GCD, etc...) Just showing you how to wire up the basics -- but you will definitely want to learn how to use AFNetworking on a background task, look into Core Data or NSCoding for caching, and a few other topics to do this sort of thing correctly.
Suffice it to say that in a proper solution:
- You don't want to be calling your web service synchronously
- You also don't want to be re-requesting the same data every time - ie don't re-download the same record from the service unless its changed
- I am not showing how to do those things here because its way beyond the scope; look a the book recommendation below as well as this link to get an idea about these topics Ray Wenderlich - sync Core Data with a web service
For your data services code, I would create a 'store' class. (do yourself a favor and get the Big Nerd Ranch iOS book if you don't already have it.
iOS Programming 4th Edition
Take the following the code with a grain of salt - for reasons I can't go into I am not able to do this from my Mac (on a Win machine) and I also am not able to copy or even email myself the code ... so I am doing all in the StackOverflow editor...
My iTunesStore contract (header file) would look something like:
// iTunesStore.h
#import <Foundation/Foundation.h>
#interface iTunesStore : NSObject
- (NSUInteger)recordCount;
- (NSDictionary*)recordAtIndex:(NSUInteger)index; // could be a more specialized record class
+ (instancetype)sharedStore; // singleton
#end
...and the implementation would look something like:
// iTunesStore.m
#import "iTunesStore.h"
// class extension
#interface iTunesStore()
#property (nonatomic, strong) NSArray* records;
#end
#implementation iTunesStore
-(id)init
{
self = [super init];
if(self) {
// DO NOT DO IT THIS WAY IN PRODUCTION
// ONLY FOR DIDACTIC PURPOSES - Read my other comments above
[self loadRecords];
}
return self;
}
- (NSUInteger)recordCount
{
return [self.records count];
}
- (NSDictionary*)recordAtIndex:(NSUInteger)index
{
NSDictionary* record = self.records[index];
}
-(void)loadRecords
{
// simulate loading records from service synchronously (ouch!)
// in production this should use GCD or NSOperationQue to
// load records asynchrononusly
NSInteger recordCount = 10;
NSMutableArray* tempRecords = [NSMutableArray arrayWithCapacity:recordCount];
// load some dummy records
for(NSInteger index = 0; index < recordCount; index++) {
NSDictionary* record = #{#"id": #(index), #"title":[NSString stringWithFormat:#"record %d", index]};
[tempRecords addObject:record];
}
self.records = [tempRecords copy];
}
// create singleton instance
+ (instancetype)sharedStore
{
static dispatch_once_t onceToken;
static id _instance;
dispatch_once(&onceToken, ^{
_instance = [[[self class] alloc] init];
});
return _instance;
}
#end
I now have a 'store' object singleton I can use to get records, return a given record and also tell me a record count. Now I can move a lot of the logic doing this from the viewcontroller.
Now I don't need to do this in your VC viewDidLoad method. Ideally, you would have an async method in your store object to get records and a block to call you back once records are loaded. Inside the block you reload records. The signature for something like that 'might' look like:
[[iTunesStore sharedStore] loadRecordsWithCompletion:^(NSError* error){
... if no error assume load records succeeded
... ensure we are on the correct thread
[self.tableView reloadData]; // will cause table to reload cells
}];
Your view controller data source methods now look like:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection(NSInteger)section {
[[iTunesStore sharedStore] recordCount];
}
Inside cellForRowAtIndexPath - I also call my store object to get the correct record
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ... get cell
// ... get record
NSDictionary* record = [[iTunesStore sharedStore] recordAtIndex:indexPath.row];
// ... configure cell]
return cell;
}
That's the gist of it. Other things to do, as noted above would be:
Have ITunesStore implement UITableViewDataSource and then just directly handle the tableview datasource methods - if you do this you don't want to make iTunesStore a singleton. And you would set an instance of iTunesStore as the tableview's delegate, rather than the viewcontroller. There are pros and cons to such an approach.
I haven't shown any real async behavior or caching which this app is crying out for
This does show how to pull off some of your model responsibilities and separate some of the tableview data source concerns.
Hopefully this will help to give you some ideas about different directions you might explore.
Happy coding!
In terms of UITableViewController, typically all the roles Model, View and Controller (MVC) is played by your UITableViewController itself. That is the case with your code as well.
As Model - It supplies data to your table view.
As Controller - It controls the look and feel of the table like number of rows, sections, height and width of them etc., supplies data from model to table view
As View - Its view property holds the UITableView
Now, to adopt a different approach you could have Model separated out from your controller class. For that have a subclass from NSObject and have it set its state which could be used by Controller.
Hope this makes sense to you.
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.
As the title implies, my UICollectionView doesn't update and display the cells immediately after calling reloadData. Instead, it seems to eventually update my collection view after 30-60 seconds. My setup is as follows:
UICollectionView added to view controller in Storyboard with both delegate and dataSource setup for the view controller and standard outlet setup
numberOfSectionsInRow & cellForItemAtIndexPath are both implemented and reference the prototyped cell and the imageView inside of it
Here is the code that goes to Twitter, get's a timeline, assigns it to a variable, reloads a table view with the tweets and then goes through the tweets to find photos and reloads the collection view with those items.
Even if I comment out the code to display the image, it still doesn't change anything.
SLRequest *timelineRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:timelineURL parameters:timelineParams];
[timelineRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if(responseData) {
JSONDecoder *decoder = [[JSONDecoder alloc] init];
NSArray *timeline = [decoder objectWithData:responseData];
[self setTwitterTableData:timeline];
for(NSDictionary *tweet in [self twitterTableData]) {
if(![tweet valueForKeyPath:#"entities.media"]) { continue; }
for(NSDictionary *photo in [[tweet objectForKey:#"entities"] objectForKey:#"media"]) {
[[self photoStreamArray] addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[photo objectForKey:#"media_url"], #"url",
[NSValue valueWithCGSize:CGSizeMake([[photo valueForKeyPath:#"sizes.large.w"] floatValue], [[photo valueForKeyPath:#"sizes.large.h"] floatValue])], #"size"
, nil]];
}
}
[[self photoStreamCollectionView] reloadData];
}
}];
This is a classic symptom of calling UIKit methods from a background thread. If you view the -[SLRequest performRequestWithHandler:] documentation, it says the handler makes no guarantee of which thread it will be run on.
Wrap your call to reloadData in a block and pass this to dispatch_async(); also pass dispatch_get_main_queue() as the queue argument.
You need to dispatch the update to the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self.photoStreamCollectionView reloadData];
});
or in Swift:
dispatch_async(dispatch_get_main_queue(), {
self.photoStreamCollectionView.reloadData()
})
Apple say:You should not call this method in the middle of animation blocks where items are being inserted or deleted. Insertions and deletions automatically cause the table’s data to be updated appropriately.
In face: You should not call this method in the middle of any animation (include UICollectionView in the scrolling).
so, you can:
[self.collectionView setContentOffset:CGPointZero animated:NO];
[self.collectionView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
or mark sure not any animation, and then call reloadData;
or
[self.collectionView performBatchUpdates:^{
//insert, delete, reload, or move operations
} completion:nil];