My problem is that the ProgressView only shows up in the first 2 cells. What is wrong with my code?
Note: My CollectionView scrolls horizontally and each cell covers the whole screen. I think this might have something to do since I tried showing all cells in the same view and they work fine. All ProgressViews show.
EDITED CODE:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.02];
PFFile *storeLooks = [self.vestimenta objectForKey:[NSString stringWithFormat:#"image_%ld", (long)indexPath.item]];
NSMutableString *precio = [NSMutableString string];
for (NSString* precios in [self.vestimenta objectForKey:[NSString stringWithFormat:#"precios_%ld", (long)indexPath.item]]) {
[precio appendFormat:#"%#\n", precios];}
[storeLooks getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error && data.length > 0) {
cell.imageFile.image = [UIImage imageWithData:data];
} else {
cell.progressView.hidden = YES;
}
} progressBlock:^(int percentDone) {
float percent = percentDone * 0.02;
[cell.progressView setProgress:percent];
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
}];
return cell;
}
Instead of removing the progress view from the cell, you should simply setHidden:YES.
This way when the cells are reused, the progress view will be present and you can then setHidden:NO when you want to start loading stuff in that cell.
Also be careful with progress blocks inside your cells when they are reused. Remember to either cancel the loading operation if the cell is reused, or make sure the progress block only continues updating it's cell if the cell hasn't been reused for a different data item.
So for example I would set the progress to 0. where you're showing the progress view like so:
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.];
And because you're hiding the progressView when the data is finished loading, I would just get rid of this code in the progressBlock:
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
I think this is related to the cells being reused as they come on screen. Since you are removing the progressView from the cell [cell.progressView removeFromSuperview] , once it is dequeued, it is still missing. You could try overriding the prepareForReuse method in your VestimentaDetailCell class to add it back so that all dequeued cells are brought back to their original state.
Related
I'm using a PFQueryTableViewController with Parse in my IOS 8 Objective-c iPhone app.
My list consists of a label and a UIImageView where both the label text and image are downloaded from a row in my Parse core. I'm using this code to achieve this:
- (PFQuery *)queryForTable
{
PFQuery *query = [PFQuery queryWithClassName:#"Story"];
return query;
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return [[self objects] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
static NSString *simpleTableIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
// Download the header image from parse
PFFile *imageFile = [object objectForKey:#"Image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *cellImage = [UIImage imageWithData:imageData];
// Set the cellImage to the cell if it's not nil
if (cellImage == nil) {
// nil - do nothing
NSLog(#"nil");
} else {
NSLog(#"not nil");
// Set the image
UIImageView *cellImageView = (UIImageView *)[cell viewWithTag:40];
cellImageView.image = cellImage;
}
}
}];
// Configure the cell
UILabel *nameLabel = (UILabel*) [cell viewWithTag:10];
nameLabel.text = [object objectForKey:#"Title"];
nameLabel.textColor = [UIColor whiteColor];
// Make the cell transparent
cell.backgroundColor = [UIColor clearColor];
cell.backgroundView = [UIView new];
cell.selectedBackgroundView = [UIView new];
// Resize the cell
[cell sizeToFit];
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Hide the tabBar and show the readButton
[self hideTabBar:self.tabBarController];
// Segue over to the viewing page
[self performSegueWithIdentifier:#"detailSegue" sender:self];
// Get the tapped cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
NSString *title = ((UILabel*) [cell viewWithTag:10]).text;
// Set selectedStory
MyManager *sharedManager = [MyManager sharedManager];
sharedManager.selectedStory = title;
// Set openedStory to YES as we opened a story
openedStory = YES;
}
This code works good, but the scrolling is a bit laggy, which I think is because it's downloading the image whenever the cell is shown. I thought of created a simple solution by creating an array of images locally and have them only download once, but it has to load 1 time minimum when the app launches. I need to somehow run the download method asynchronously (or another solution that would work).
How can I achieve this?
(I'm using storyboards)
EDIT
Thanks in advance!
Erik
EDIT 2:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (![self.shownIndexes containsObject:indexPath]) {
[self.shownIndexes addObject:indexPath];
UIView *weeeeCell = [cell contentView];
weeeeCell.layer.transform = self.initialTransform;
weeeeCell.layer.opacity = 0.8;
[UIView animateWithDuration:1.25 delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:0.5 options:0 animations:^{
weeeeCell.layer.transform = CATransform3DIdentity;
weeeeCell.layer.opacity = 1;
} completion:^(BOOL finished) {}];
}
}
and
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
}
Your hypothesis about the problem is right, and your idea about a solution is right, too. The additional requirement that you mention about preloading the images is a little fuzzy.
Must they be loaded before the table appears? If they are loaded asynchronously, which they should be, then you'll need to block user's access to the table until the requests are complete. You're replace the poor experience of not seeing the images right away with the worse experience of not seeing the table at all.
I think the better answer is to just load lazily. The outline of the solution is:
Declare a dictionary of images (to be indexed by the indexPaths) and be sure to initialize it to an empty dictionary...
#interface MyViewController () // replace 'MyViewController' with your class
#property(strong,nonatomic) NSMutableDictionary *images;
#end
Use that collection in cellForRowAtIndexPath...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
static NSString *simpleTableIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
UIImageView *cellImageView = (UIImageView *)[cell viewWithTag:40];
UIImage *cachedImage = self.images[indexPath];
if (cachedImage) {
cellImageView.image = cachedImage;
} else {
cellImageView.image = // put a place holder image here
// load lazily, but read on. the code in the callback should assume
// nothing about the state of the table when it runs
PFFile *imageFile = [object objectForKey:#"Image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
// what if this gets run a second time before the first request finishes?
// no worries, check for that here:
if (!error && !self.images[indexPath]) {
UIImage *cellImage = [UIImage imageWithData:imageData];
self.images[indexPath] = cellImage;
// this is important: don't refer to cell in here, it may be
// scrolled away and reused by the time this closure runs
// the code we just wrote to init the cellImageView works just fine
// call that using reload
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}];
}
// Configure the cell
UILabel *nameLabel = (UILabel*) [cell viewWithTag:10];
nameLabel.text = [object objectForKey:#"Title"];
nameLabel.textColor = [UIColor whiteColor];
// Make the cell transparent
cell.backgroundColor = [UIColor clearColor];
cell.backgroundView = [UIView new];
cell.selectedBackgroundView = [UIView new];
// Resize the cell
[cell sizeToFit];
return cell;
}
Edit -- don't bother with this for now, but -- if you really do have the opportunity to prepare the view before its shown (like maybe this view controller is in a tab bar container and not the default tab). You could use the table view helper methods to do a pre-fetch of the visible rows...
- (void)prepareToBeShown {
NSArray indexPaths = [self.tableView indexPathsForVisibleRows];
[self.tableView reloadRowsAtIndexPaths:indexPaths];
}
EDIT 2:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (![self.shownIndexes containsObject:indexPath]) {
[self.shownIndexes addObject:indexPath];
UIView *weeeeCell = [cell contentView];
weeeeCell.layer.transform = self.initialTransform;
weeeeCell.layer.opacity = 0.8;
[UIView animateWithDuration:1.25 delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:0.5 options:0 animations:^{
weeeeCell.layer.transform = CATransform3DIdentity;
weeeeCell.layer.opacity = 1;
} completion:^(BOOL finished) {}];
}
}
Have you thought about using a PFImageView instead of a UIImageView?
All you have to do it set it's file and tell it to load in the background. I've never had any lag when using them in my tableviews.
I have a UICollectionView that holds 10 items (images retrieved from Parse.com) Every cell covers the entire screen and it is set as a Horizontal Scroll.
Inside the cell I have a 'Like Button' that when clicked it changes color. The problem is that if I click the like button in cell 1 then cell 3, cell 5, and so on also changes the buttons color.. and if I click on cell 0 then cell 2, cell 4 and so on changes it color also.
I tried changing the UICollectionView cells size and made is so that the 10 cells fit in the screen. When I did that, no buttons repeated. The only change was in the button I clicked. Any thoughts as why this is happening and how can I fix it? I need the Cells to be the entire Screen.
My Code:
VestimentaDetailViewController.m
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 10;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.02];
cell.imageFile.image = [UIImage imageNamed:#"loadingLook.png"];
PFFile *storeLooks = [self.vestimenta objectForKey:[NSString stringWithFormat:#"image_%ld", (long)indexPath.item]];
[storeLooks getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error && data.length > 0) {
cell.imageFile.image = [UIImage imageWithData:data];
} else {
cell.progressView.hidden = YES;
}
} progressBlock:^(int percentDone) {
float percent = percentDone * 0.02;
[cell.progressView setProgress:percent];
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
}];
return cell;
}
VestimentaDetailCell.m
- (IBAction)likeLook:(id)sender {
if ([sender isSelected]) {
[sender setImage:[UIImage imageNamed:#"Like.png"] forState:UIControlStateNormal];
[sender setSelected:NO];
} else {
[sender setImage:[UIImage imageNamed:#"Liked.png"] forState:UIControlStateSelected];
[sender setSelected:YES];
NSLog(#"Liked Image");
}
}
Collection and table views recycle their cells. I think what's happening is that if you like cell 1 then cell 3, cell 5, so on... are all using a recycled cell 1.
To fix this, set the button color for all cells the are being dequeued into cellForRowAtItemPath to be the default button color.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// get cell and stuff
...
cell.likeButton.backgroundColor = [UIColor whiteColor];
}
I'm trying to create CollectionView similar to 500px app.
App logic: After app is launched, CollectionView loads empty cells(around 100), then when user scrolls down, cells are filled with image. At the end of view, new batch of empty cells are created(Load More). Images are pulled from Instagram, 20 per call.
Problem: When new images are pulled(async), cell rendering continues and usually it manages to create 3-5 empty cells before next batch of images are available.
Question: Is there any solutions for this problem? Maybe there's a better way to achieve this?
Code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ALPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"photo" forIndexPath:indexPath];
cell.backgroundColor = [UIColor grayColor];
if (indexPath.row < [self.photos count]) {
cell.photo = self.photos[indexPath.row];
}
if (indexPath.row == [self.photos count]-6) {
[ALLoadMore loadMore:self.nextPage input:self.photos completionHandler:^(NSArray *newLoad, NSString *next) {
self.photos = newLoad;
self.nextPage = next;
NSLog(#"%#", self.nextPage);
dispatch_async(dispatch_get_main_queue(), ^{
//[self.collectionView reloadData];
if ([self.photos count] > indexPath.row) {
cell.photo = self.photos[indexPath.row];
NSLog(#"call");
}
NSLog(#"%lu", (unsigned long)[self.photos count]);
});
}];
}
return cell;}
For now cell count is a static number:120
I have a problem with cellForRowAtIndexPath always returning nil when called from tableView:cellForRowAtIndexPath:
The thing is I am actually calling cellForRowAtIndexPath inside a block inside tableView:cellForRowAtIndexPath: and I guess that this might be the problem.
I am populating cells with remote images that load (and cache) ad hoc. When the image returns fully loaded in the completion handler block (inside tableView:cellForRowAtIndexPath:) I need to get the cell again because it might have scrolled out of view... So I call cellForRowAtIndexPath to get the cell again - cellForRowAtIndexPath would only return a cell if it's visible. In my case I never get it to return anything but nil. Even though I am scrolling very slowly (or fast, or whatever speed I tried...) all I get is nil.
I'll try to get some code in here soon but until then any suggestion why this would occur - I guess the block thing would be a hint.
Here is the code: https://dl.dropboxusercontent.com/u/147970342/stackoverflow/appsappsappsappsapps.zip
The -tableView:cellForRowAtIndexPath: implementation:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber* numberAppleid = self.appids[indexPath.row];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
cell.imageView.hidden = YES; // Hide any previous until image loads
[self getAppIconForAppleid:appleidasstring handlerImage:^(UIImage *image) {
if (image == nil) {
return;
}
DLog(#"cellForRowAtIndexPath tableView: %#, indexPath: %#", tableView, indexPath);
UITableViewCell* localcell = [tableView cellForRowAtIndexPath:indexPath];
if (localcell != nil) {
localcell.imageView.image = image;
localcell.imageView.hidden = NO;
}
else {
DLog(#"Nah indexpath is not visible for: %# because localcell is nil.", appleidasstring);
}
}];
return cell;
}
Fixed version:
#define KEYAPPLEID #"keyappleid"
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber* numberAppleid = self.appids[indexPath.row];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
[cell setAssociativeObject:appleidasstring forKey:KEYAPPLEID];
cell.imageView.hidden = YES; // Hide any previous until image loads
[self getAppIconForAppleid:appleidasstring handlerImage:^(UIImage *image) {
if (image == nil) {
return;
}
NSString* currentappleidofcell = [cell associativeObjectForKey:KEYAPPLEID];
if ([currentappleidofcell isEqualToString:appleidasstring]) {
cell.imageView.image = image;
cell.imageView.hidden = NO;
}
else {
DLog(#"Returning appleid: %# is not matching current appleid: %#", appleidasstring, currentappleidofcell);
}
}];
return cell;
}
And this category is needed: https://stackoverflow.com/a/10319083/129202
If the block is within the tableView:cellForRowAtIndexPath: callback, you can just reference the cell directly in the block. The block will automatically keep the cell around until the block goes away. However, be careful of retain cycles. If the block is owned by the cell, you will have to use a weak reference to the cell.
So your implementation would look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber* numberAppleid = self.appids[indexPath.row];
// Create your own UITableViewCell subclass that has an "appleidasstring" property
MyTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"Mycell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"Appleid: %#", numberAppleid];
NSString* appleidasstring = [NSString stringWithFormat:#"%#", numberAppleid];
cell.imageView.hidden = YES; // Hide any previous until image loads
// Set the appleidasstring on the cell to be checked later
cell. getAppIconForAppleid:appleidasstring = appleidasstring;
// Modify the handler callback to also callback with the appleidasstring
[self
getAppIconForAppleid:appleidasstring
handlerImage:^(UIImage *image, NSString *imageAppleIdaString)
{
if (image == nil) {
return;
}
// Ensure the cell hasn't been repurposed for a
// different imageAppleIdaString
if ([cell.appleidasstring isEqualToString:imageAppleIdaString]) {
cell.imageView.image = image;
cell.imageView.hidden = NO;
}
}
];
return cell;
}
I have a horizontal scrolling table view. Each cell takes the whole screen to show full pictures. I'm doing lazy loading of the pictures. The problem I'm having is that when the pictures finish downloading, the UIImageview.image from the cell is not updated automatically. I need to scroll to another cell and back for it to show.
Here is the method called when the picture is finished downloading:
- (void)imageDownloaderDidFinish:(PictureDownloadOperation *)downloader {
NSLog(#"image download finished");
NSIndexPath *indexPath = downloader.indexPathInTableView;
PictureRecord *theRecord = downloader.photoRecord;
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath];
}
This is the cellforwoeatindexpath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"HorizontalCell";
HorizontalTableCell *cell = [self.myTableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[HorizontalTableCell alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width)];
}
cell.contentView.transform = CGAffineTransformMakeRotation(-M_PI/-2);
PictureRecord *aRecord = [self.photos objectAtIndex:indexPath.section];
[cell.indicator startAnimating];
if (aRecord.hasImage) {
[cell.indicator stopAnimating];
[cell.pic setImage:aRecord.image];
[cell setNeedsDisplay];
cell.username.text = aRecord.username;
}
else if (aRecord.isFailed) {
[cell.indicator stopAnimating];
cell.pic.image = [UIImage imageNamed:#"first.png"];
}
else {
[cell.indicator startAnimating];
if (!myTableView.dragging && !myTableView.decelerating) {
[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];
}
}
return cell;
}
Thanks
UPDATE:
One more information about this problem. After troubleshooting I found that the problem is with reloadRowsAtIndexPaths. It does not call cellForRowAtIndexPath for the image to load. However I'm still not sure it does not call cellForRowAtIndexPath. I have also tried [tableview reloadData] and no luck.
FIXED IT:
The problem was fixed by adding another tableview inside this existing table view. The way I had before was a UITableView inside a UIView. I modified it so I had one UITableView with one cell. Created a custom cell and added another table in that cell so I could scroll it horizontally and added another custom cell for the horizontal tableview with the imageview. That did the trick.