I have to reload the data in the collection view, in order to set a new cells sizes according to the new data source .
Than i have to scroll to the start, and i would like to do that with animation .
every row has 1 cell in it .
So, using this :
[self.collectionView reloadData];
[self.collectionView setContentOffset:CGPointZero animated:YES];
Whats happens is that it scrolls to the start, and reloaded, but when he comes to image 1 , than i see many images replaced fast, and it stopes on some image that is not belong to the first cell, but to other cell .
If i scroll to the start- without animation (animated:NO) its not happens .
I need that animation.
What could cause this problem ?
EDIT:
I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.
I think i solve the problem, or improved things a lot .
Well when you start downloading from the net, give your block a tag :
- (void)downloadImageWithURL:(NSURL *)url AndTag:(int)Gtag completionBlock:(void (^)(BOOL succeeded, NSData *data,int tag))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
int thetag=Gtag;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (!error)
{
completionBlock(YES, data,thetag);
}
else
{
completionBlock(NO, nil,thetag);
}
}];
}
Than, when the download complete, we will check if the cell is on screen right now , if its not, we will not load its image to the cell :
[self downloadImageWithURL:url AndTag:(int)cell.tag completionBlock:^(BOOL succeeded, NSData *data,int tag)
{
BOOL isvisible=0;
for (iPhoneCostumCell *tcell in [self.collectionView visibleCells])
{
if(tcell.tag==tag)
{
isvisible=1;
break;
}
}
I think this is a good solution , and it makes things more stable . It also makes the processor work less, because if we dont find the image, we dont continue to the NSData conversion-made in another thread .
The problem is that cells are reused,
So let's see this example
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
cell.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *image = [self imageForIndexPathRow:#(indexPath.row)];
dispatch_async(dispatch_get_main_queue(), ^{
cell.image = image;
});
});
return cell;
}
As the operations are asynchronous, scrolling fast will cause to dispatch 2 operations for 1 cell.
For example lets name some cell pointer "ImageCell-1"
Now lets see, what will be, if you start fast scrolling, and 2 operations are dispatched.
First operation loads "a.png", and download is active, you scroll fast and "ImageCell-1" is now being loaded "b.png" (but "a.png" is still loading).
So "a.png" completes download and is set to "ImageCell-1", then completes "b.png" and is set to "ImageCell-1".
(this is the issue that you see => "I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.")
But also can be situation (when you use "high level libraries" like AFNetworking, when queues are managed by them, and concurrency downloading is available) "b.png" completes download and sets it to "ImageCell-1", after that completes "a.png" download and set to "ImageCell-1". This will cause to see is "a.png" instead of "b.png".
The solution is to cancel download operation, before starting new one, if you are using AFNetworking, keep NSOperations, and call [operation cancelOperation]; before starting new download.
EDIT:
If you're using NSURLConnection, you need to switch to modern way of using NSURLSession (to support request canceling).
Here I have wrote a small method, which resolves your problem
#interface MyCell ()
#property (strong, nonatomic) NSURLSessionDataTask *dataTask;
#end
- (void)loadImageFromPath:(NSString *)aPath
availableCache:(NSCache *)aCache
{
if (self.dataTask) {
[self.dataTask cancel];
}
NSData *cache = [aCache objectForKey:aPath];
if (cache) {
self.image = [UIImage imageWithData:cache];
return;
}
self.dataTask = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:aPath]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error)
{
if (data) {
self.image = [UIImage imageWithData:data];
self.dataTask = nil;
// Add to cache
[aCache setObject:data forKey:aPath];
}
}];
[self.dataTask resume];
}
Place this method in your Cell,
And call it from cellForRow, like this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
[cell loadImageFromPath:imagePath availableCache:self.cache];
return cell;
}
[UIView animateWithDuration:1.0 delay:0.0 options: UIViewAnimationOptionCurveEaseIn animations:^{
[self.collectionView setContentOffset:CGPointZero animated:NO];
} completion:nil];
}];
Try to make your own animation.
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
At this point I'm really fed up. It's been nearly a week now trying to solve this issue so I can move ahead. I've read multiple threads and done multiple searches in regards to my slow loading choppy UICollectionView.
I've tried to do this without any libraries as well as with SDWebImage and AFNetwork. It still doesn't fix things. Images loading isn't really a problem. The problem arrives when I scroll to cells that aren't currently showing on the screen.
As of now I've deleted all the code and all traces of any libraries and would like to get help in order to implement this properly. I've made about 2 posts on this already and this would be my third attempt coming from a different angle.
Information
My backend data is stored on Parse.com
I have access to currently loaded objects by calling [self objects]
My cellForItemAtIndex is a modified version that also returns the current object of an index.
From what I understand in my cellForItemAtIndex I need to check for an image, if there isn't one I need to download one on background thread and set it so it shows in the cell, then store a copy of it in cache so that if the associated cell goes off screen when I do scroll back to it I can use the cached image rather than downloading it again.
My custom parse collectionViewController gives me all the boiler plate code I need to get access to next set of objects, current loaded objects, pagination, pull to refresh etc. I really just need to get this collection view sorted. I never needed to do any of this with my tableview of a previous app which had much more images. It's really frustrating spending a whole day trying to solve an issue and getting no where.
This is my current collectionView cellForItemAtIndex:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
// check for image
// if there is a cached one use that
// if not then download one on background thread
// set my cells image view with that image
// cache image for re-use.
// PFFile *userImageFile = object[#"image"];
[[cell title] setText:[object valueForKey:#"title"]]; //title set
[[cell price] setText:[NSString stringWithFormat: #"£%#", [object valueForKey:#"price"]]]; //price set
return cell;
}
I am also using a custom collectionViewCell:
#interface VAGGarmentCell : UICollectionViewCell
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#property (weak, nonatomic) IBOutlet UITextView *title;
#property (weak, nonatomic) IBOutlet UILabel *price;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
If there's any more information you'd like please ask. I'd just like a clear example in code of how to do this correctly, if it still doesn't work for me then I guess there is something wrong some where within my code.
I'm going to continue reading through various threads and resources I've come across in the last few days. I can say one benefit in this experience is that I have a better understanding of threads and lazy loading but it is still very frustrated that I have made any progress with my actual app.
Incase you wondered here is my previous post: In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?
I'd either like to do this quick and manually or using the AFNetwork as that didn't cause any errors or need hacks like SDWebImage did.
Hope you can help
Kind regards.
You can make use of the internal cache used by NSURLConnection for this.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"VAGGarmentCell" forIndexPath:indexPath];
//Standard code for initialisation.
NSURL *url; //The image URL goes here.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0]; //timeout can be adjusted
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *image = [UIImage imageWithData:data];
//Add image as subview here.
}
}];
.
.
return cell;
}
This is for a table view, but same concept basically. I had the same issue you were having. I had to check for a cached image, if not, retrieve it from a server. The main thing to watch out for is when you retrieve the image back, you have to update it in the collection view on the main thread. You also want to check if the cell is still visible on the screen. Here is my code as an example. teamMember is a dictionary and #"avatar" is the key which contains the URL of the user's image. TeamCommitsCell is my custom cell.
// if user has an avatar
if (![teamMember[#"avatar"] isEqualToString:#""]) {
// check for cached image, use if it exists
UIImage *cachedImage = [self.imageCache objectForKey:teamMember[#"avatar"]];
if (cachedImage) {
cell.memberImage.image = cachedImage;
}
//else retrieve the image from server
else {
NSURL *imageURL = [NSURL URLWithString:teamMember[#"avatar"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
// if valid data, create UIImage
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
// if valid image, update in tableview asynch
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
// if valid cell, display image and add to cache
if (updateCell) {
updateCell.memberImage.image = image;
[self.imageCache setObject:image forKey:teamMember[#"avatar"]];
}
});
}
}
});
}
}
NSURLCache is iOS's solution to caching retrieved data, including images. In your AppDelegate, initialize the shared cache via:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:cache];
return YES;
}
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
Then use AFNetworking's UIImageView category to set the image using:
[imageView setImageWithURL:myImagesURL placeholderImage:nil];
This has proven to load images the second time around incredibly faster. If you are worried about loading images faster for the first time, you will have to create a way to determine when and how many images you want to load ahead of time. It is very common to load data using paging. If you are using paging and still are having trouble, consider using AFNetworking's:
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
This way you can create an array of UIImages and using this method to return the images for each cell before dequeuing the cell. So in this case you would have two parallel arrays; one holding your data and the other holding corresponding UIImages. Memory management will eventually get out of hand so keep that in mind. If someone scrolls quickly to the bottom of the available cells, there is honestly not much else you can do since the data depends on the network connection of the user.
After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.
I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.
I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.
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.
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];
I am using https://github.com/rs/SDWebImage to load images in a UITableView.
Here is how i implemented it (simple), inside cellForRowAtIndexPath
[cell.imageView setImageWithURL:[NSURL URLWithString:[item valueForKey:#"icon"]]placeholderImage:[UIImage imageNamed:#"icon_events_default.png"]];
After images are loaded in UITableView, i scroll down, and than again up, and i receive error:EXC_BAD_ACCESS
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[manager cancelForDelegate:self];
UIImage *cachedImage = [manager imageWithURL:url];
if (cachedImage)
{
//EXC_BAD_ACCESS hapens here
self.image = cachedImage;
}
else
{
if (placeholder)
{
self.image = placeholder;
}
[manager downloadWithURL:url delegate:self];
}
}
Any help is really appreciated.
Did you run this code through Zombies in Instruments? That should point to the problem immediately. Just select Profile from the Product menu, Instruments will start, select the Zombie instrument, then run the test scenario which causes this problem, and you should see a zombie pop up that shows how an object is still being used even though it's no longer valid.
If I had to guess, you're UITableViewCell is not being properly retained and it's either getting released or reused too quickly before the image at the url loads.