I want to create an effect so that my collectionviewcell (which has one imageview as IBOutlet) will load its image with a random delay. So for example, cell #1 will have its image shown in 2 seconds, while #2 will take 1 second, cell #3 will take 4 seconds ... etc.
How would I do this? I heard about using NSOperationQueue at cellForRowAtIndexPath but not sure how to implement.
If your images are already available, you could try giving a random delay in your cellForItemAtIndexPath method:
UIImage *theImage = ...; //get your image
int maxDelay = 4;
int delayInSeconds = arc4random() % maxDelay;
cell.imageView.image = theImage;
cell.imageView.alpha = 0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
cell.imageView.alpha = 1;
});
If your image is not available in memory, and it needs to be loaded, you may want to look at this post on how to load the image asynchronously:
You could have a method on the cell's subclass for setting the image, then within that method call another method with a random delay to actually set the UIImageView's image. Something like this:
- (void)delayedSetBackgroundImage:(UIImage *)image
{
[self performSelector:#selector(setBackgroundImage:) withObject:image afterDelay:(arc4random() % 5)];
}
- (void)setBackgroundImage:(UIImage *)image
{
_myImageView.image = image;
}
So then in cellForRowAtIndexPath you would call delayedSetBackgroundImage: and the cell itself would take care of the rest.
Related
I have an interface which contains several UIImageView components on a view and i would like to update the animation images in run-time.
The main idea is to change the animation images into a thread with a delay of 2 seconds. The code below works fine. However, when i introduce a sleep time (with the commented NSThread sleepForTime); it no longer works. I am sure there is a logical explanation but i cannot identify it.
It's important to note that the animation is already running with another set of animated images.
Any help or tip is more than welcome :)
dispatch_async(allowToTouchThread, ^{
//[NSThread sleepForTimeInterval:2.0];
int randomnReward = 0;
WizzReward* currentReward = [rewardListForPages objectAtIndex:randomnReward];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
float animationDuration = [animalModel getStandardRewardAnimationDuration];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.animationLevel1Duration = animationDuration;
}
});
What about replacing your dispatch_async to a dispatch_after and set a delay of 2 seconds:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
int randomnReward = 0;
WizzReward* currentReward = [rewardListForPages objectAtIndex:randomnReward];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
float animationDuration = [animalModel getStandardRewardAnimationDuration];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.animationLevel1Duration = animationDuration;
});
Changing the animated images without any timer, could be done directly by modifying the array of images.
However, changing the same animated images with a timer, needed 3 steps:
first the modification of the image arrays
second the actual re-association of the UIView AnimationImages to the modified array of images;
and finally startAnimating the UIView eventhough it hasn't been stopped beforehand
The source code below now works fine with a timer:
WizzReward* currentReward = [rewardListForPages objectAtIndex:0];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.reward1CurrentPage.animationImages = pageContentViewController.imagesArrayFileLevel1;
[pageContentViewController.reward1CurrentPage startAnimating];
I did find neither "time vs no-timer difference"information in the literature nor actual reason why it should be done like that; but it works every time. Hope it helps anyone who runs into the same problem.
I'm working in a project (iOS7 & ARC) in which, I want to display N number of images in the scroll view.These Images already stored into sandbox directory. My App has only landscape orientation I'm facing a problem that ScrollView is not smooth, it stuck after 2-3 times scroll
This is how I configure ScrollView
[self.containerScroll setAutoresizesSubviews:NO];
self.containerScroll.pagingEnabled = YES;
self.containerScroll.showsHorizontalScrollIndicator = NO;
self.containerScroll.showsVerticalScrollIndicator = NO;
self.containerScroll.scrollsToTop = NO;
self.containerScroll.maximumZoomScale = 5.0;
self.containerScroll.minimumZoomScale = 1.0;
self.containerScroll.delegate = self;
I'm maintaining only three Images in the scrollView at a time.
I'm loading Images in ScrollView in below method
-(void) loadScrollViewWithPage:(int) page{
if (page >= self.numberOfSlides)
return;
float image_width;
float image_height;
if(self.isFromListView){
if(IS_IPHONE5){
image_width = 568.0f;
image_height = 320.0f;
} else{
// iPhone retina-3.5 inch
image_width = 480.0f;
image_height = 320.0f;
}
}
else{
image_width = IMAGE_WIDTH;
image_height = IMAGE_HEIGHT;
}
CGFloat xPos = page * image_width;
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(xPos, 0.0f, image_width, image_height)];
imgView.tag = page;
NSString *imgPath = [self.storageDirPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%d%#", page, Image_Extension_JPG]];
NSFileManager *fileManager = [NSFileManager defaultManager];
__block UIImage *img = nil;
if(![fileManager fileExistsAtPath:imgPath]){
[imgView setContentMode:UIViewContentModeCenter];
img = [UIImage imageNamed:#"icon-loader.png"];
[imgView setImage:img];
}
else{
[imgView setContentMode:UIViewContentModeScaleAspectFit];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
img = [[UIImage alloc] initWithCGImage:[[UIImage imageWithData:[NSData dataWithContentsOfFile:imgPath]] CGImage] scale:1.0 orientation:UIImageOrientationUp];
dispatch_async(dispatch_get_main_queue(), ^{
[imgView setImage:img];
});
});
}
[self.containerScroll addSubview:imgView];
img = nil;
fileManager = nil;
imgView = nil;
}
and this how my ScrollView Delegate methods goes...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
self.containerScroll.scrollEnabled = YES;
float page = self.containerScroll.contentOffset.x/self.view.frame.size.width;
showingSlide = (UInt16) roundf(page);
if(scrollView == self.containerScroll){
// switch the indicator when more than 50% of the previous/next page is visible
CGFloat pageWidth = CGRectGetWidth(self.containerScroll.frame);
NSUInteger pageNo = floor((self.containerScroll.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
// load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
[self loadScrollViewWithPage:pageNo - 1];
[self loadScrollViewWithPage:pageNo];
[self loadScrollViewWithPage:pageNo + 1];
// a possible optimization would be to unload the views+controllers which are no longer visible
if(scrollView == self.containerScroll)
{
[self.previewTableView reloadData];
[self.previewTableView setContentOffset:CGPointMake(0, (page*220)+64) animated:NO];
[self.previewTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:page inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[self updateSlideNumber];
[self flashSlideNumber];
}
//unload unnecessary imageviews from scroll view
for (UIView* view in self.containerScroll.subviews) {
if ([view isKindOfClass:[UIImageView class]] && view.tag != page && view.tag != page-1 && view.tag != page+1) {
[view removeFromSuperview];
}
}
}
}
Now the problem is smoothness of scrollView. When I start scrolling it scrolls fine but after 2 or 3 (or after any random number) pages scroll, it stuck and after trying 2-3 times only it moves again and I have to swipe hard to scroll. Thanks in advance.
I think it's a problem of memory somewhere Try #autorelease pool in your code.
using scrollview is not a good approach for showing images, I will recommend you to either use tableview or collevtionview for the same.
Your app's memory will keep on increasing with every scroll because scollview doesn't reuse the memory, on the other hand tableview and collectionview reuses the memory.
As the most effective way to become better, scroll really slowly (one at a time) while you monitor the memory usage in your App. You'll be able to watch it go up as each new image is added to the view, especially if you haven't done any optimisation on the images.
The other thing is that while your code does look like is deallocc-ing the images, you still need to remember that it still has to try to reload the images as you scroll. You're creating your images on the main thread so you're never going to get the smoothness of a UITableView. While I realise that you're creating your image views on async threads, the act of adding and scrolling them is still being taken care of by the mainthread.
I would suggest a UITableView to solve your problem, or a UICollectionView. If you're set on using the scrollview, I would suggest using a crusher of some type to get the image size to as small as possible, while still keeping quality decent.
If you need help on the TableView implementation you should find plenty of information around SO. Probably a good option if you still want it to look like a scroll view is just to make all seperators, headers etc to nil, and then just use lazy loading for the images.
You make two the mistake. At first: never use imageNamed for non graphics content (example, use imageNamed for button background). And second: you try load a big images in real time. So you scroll view have lags therefore. If you load all images before you show the scroll view the amination end lagging. But you can get memory warnings. So, you need optimise it. P.S. Sorry for my english
This question already has answers here:
Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling
(13 answers)
Closed 9 years ago.
Im using a asynchronous block (Grand central dispatch) to load my cell images. However if you scroll fast they still appear but very fast until it has loaded the correct one. Im sure this is a common problem but I can not seem to find a away around it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *offersImage = [UIImage imageWithData:data];
cell.imageView.image = offersImage;
});
});
cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];
return cell;
}
At the very least, you probably want to remove the image from the cell (in case it is a re-used cell) before your dispatch_async:
cell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
Or
cell.imageView.image = nil;
You also want to make sure that the cell in question is still on screen before updating (by using the UITableView method, cellForRowAtIndexPath: which returns nil if the cell for that row is no longer visible, not to be confused with the UITableViewDataDelegate method tableView:cellForRowAtIndexPath:), e.g.:
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
if (data) {
UIImage *offersImage = [UIImage imageWithData:data];
if (offersImage) {
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = offersImage;
}
});
}
}
});
cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];
return cell;
Frankly, you should also be using a cache to avoid re-retrieving images unnecessarily (e.g. you scroll down a bit and scroll back up, you don't want to issue network requests for those prior cells' images). Even better, you should use one of the UIImageView categories out there (such as the one included in SDWebImage or AFNetworking). That achieves the asynchronous image loading, but also gracefully handles cacheing (don't reretrieve an image you just retrieved a few seconds ago), cancelation of images that haven't happened yet (e.g. if user quickly scrolls to row 100, you probably don't want to wait for the first 99 to retrieve before showing the user the image for the 100th row).
This is a problem with async image loading...
Let's say you have 5 visible rows at any given time.
If you are scrolling fast, and you scroll down for instance 10 rows, the tableView:cellForRowAtIndexPath will be called 10 times. The thing is that these calls are faster than the images are returned, and you have the 10 pending images from different URL-s.
When the images finally come back from the server, they will be returned on those cells that you put in the async loader. Since you are reusing only 5 cells, some of these images will be displayed twice on each cell, as they are downloaded from the server, and that is why you see flickering. Also, remember to call
cell.imageView.image = nil
before calling the async downloading method, as the previous image from the reused cell will remain and also cause a flickering effect when the new image is assigned.
The way around this is to store the latest URL of the image you have to display on each cell, and then when the image comes back from the server check that URL with the one you have in your request. If it is not the same, cache that image for later.
For caching requests, check out NSURLRequest and NSURLConnection classes.
I strongly suggest that you use AFNetworking for any server communication though.
Good luck!
The reason of your flicker is that your start the download for several images during the scrolling, every time you a cell is displayed on screen a new request is performed and it's possible that the old requests are not completed, every tile a request completes the image is set on the cell, so it's if you scroll fast you use let's say a cell 3 times = 3 requests that will be fired = 3 images will be set on that cell = flicker.
I had the same issue and here is my approach:
Create a custom cell with all the required views. Each cells has it's own download operation. In the cell's -prepareForReuse method. I would make the image nil and cancel the request.
In this way for each cell I have only one request operation = one image = no flicker.
Even using AFNetworking you can have the same issue if you won't cancel the image download.
The issue is that you download the image on the main thread. Dispatch a background queue for that:
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", NULL);
// execute a task on that queue asynchronously
dispatch_async(myQueue, ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
//UI updates should remain on the main thread
UIImage *offersImage = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *offersImage = [UIImage imageWithData:data];
cell.imageView.image = offersImage;
});
});
I have this simple UITableView and each cell has an image corresponding to it. All I'm doing is displaying a title for the image and the image itself in each cell. Here is my cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// this is where the data for each cell is
NSDictionary *dataForThisCell = cachedData.posts[indexPath.row][#"data"];
// this creates a new cell or grabs a recycled one, I put NSLogs in the if statement to make sure they are being recycled, they are.
post *cell = (post *) [self.tableView dequeueReusableCellWithIdentifier:#"postWithImage"];
if (cell == nil) {
cell = [[[NSBundle mainBundle]loadNibNamed:#"postWithImage" owner:self options:nil]objectAtIndex:0];
[cell styleCell];
}
// if this cell has an image we need to stick it in the cell
NSString *lowerCaseURL = [dataForThisCell[#"url"] lowercaseString];
if([lowerCaseURL hasSuffix: #"gif"] || [lowerCaseURL hasSuffix: #"bmp"] || [lowerCaseURL hasSuffix: #"jpg"] || [lowerCaseURL hasSuffix: #"png"] || [lowerCaseURL hasSuffix: #"jpeg"]) {
// if this cell doesnt have an UIImageView, add one to it. Cells are recycled so this only runs several times
if(cell.preview == nil) {
cell.preview = [[UIImageView alloc] init];
[cell.contentView addSubview: cell.preview];
}
// self.images is an NSMutableDictionary that stores the width and height of images corresponding to cells.
// if we dont know the width and height for this cell's image yet then we need to know now to store it
// once the image downloads, and then cause our table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
Boolean shouldReloadData = self.images[dataForThisCell[#"name"]] == nil ? YES : NO;
// download image
[cell.preview cancelImageRequestOperation];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: dataForThisCell[#"url"]]];
[request addValue:#"image/*" forHTTPHeaderField:#"Accept"];
[cell.preview setImageWithURLRequest: request
placeholderImage: [UIImage imageNamed:#"thumbnailLoading.png"]
success: ^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// if we indicated earlier that we didnt know the dimensions of this image until
// just now after its been downloaded, then store the image dimensions in self.images
// and tell the table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
if(shouldReloadData) {
NSInteger imageWidth = image.size.width;
NSInteger imageHeight = image.size.height;
if(imageWidth > [ColumnController columnWidth]) {
float ratio = [ColumnController columnWidth] / imageWidth;
imageWidth = ratio * imageWidth;
imageHeight = ratio* imageHeight;
}
if(imageHeight > 1024) {
float ratio = 1024 / imageHeight;
imageHeight = ratio * imageHeight;
imageWidth = ratio* imageWidth;
}
self.images[dataForThisCell[#"name"]] = #{ #"width": #(imageWidth), #"height": #(imageHeight), #"titleHeight": #([post heightOfGivenText: dataForThisCell[#"title"]]) };
[self.tableView reloadData];
// otherwise we alreaady knew the dimensions of this image so we can assume
// that heightForRowAtIndexPath has already calculated the correct height
// for this cell
}else{
// assign the image we downloaded to the UIImageView within the cell
cell.preview.image = image;
// position the image
NSInteger width = [self.images[dataForThisCell[#"name"]][#"width"] integerValue];
NSInteger height = [self.images[dataForThisCell[#"name"]][#"height"] integerValue];
cell.preview.frame = CGRectMake( ([ColumnController columnWidth] - width)/2 , [self.images[dataForThisCell[#"name"]][#"titleHeight"] integerValue] + 10, width, height);
}
}
failure: ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {}];
}
// set title of the cell
cell.title.text = [NSString stringWithFormat: #"%#\n\n\n\n\n", dataForThisCell[#"title"]];
`enter code here`// ask for a restyle
[cell setNeedsLayout];
// returns my customized cell
return cell;
}
What happens is that everything works exactly how I want it to, however once I scroll down past around 100 cells or so the background of my app goes black for a few seconds and then I see my homescreen (I've seen some people call this the HSOD - home screen of death). Sometimes in the console in xcode I see memory warnings before a crash and sometimes I do not.
I know for a fact that whatever the problem is, it has to do with putting images into the cells. If I comment out just this line:
cell.preview.image = image;
Then everything works fine and it doesn't crash any more (but then of course the images are not being displayed in the cells).
The cells are being reused and I know that's working, for good measure I set the UIImageView's image property to nil:
- (void) prepareForReuse {
[super prepareForReuse];
if(self.preview != nil)
self.preview.image = nil;
}
and in my appDelegate I also define this:
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[UIImageView clearAFImageCache];
}
Which deletes the image cache but that doesn't fix the problem either (and anyway iOS should clear the image caches upon memory warnings automatically anyway).
I ran analyze on my project and it reports no memory leaks, and here is the profiler showing that, as well as showing the allocations at the time of the crash:
Other than the occasional memory warning in the console which appears about 2/3rds of the time the app crashes, there are no other errors that appear in the console, and I do not hit any breakpoints or exceptions.
All of those allocations are you creating new table view cells each time they're requested, rather than reusing existing ones. Without setting a reuseIdentifier for cells created from UINib, dequeueReusableCellWithIdentifier: will always return `nil.
To fix this, add the following code (as referenced in this question):
[self.tableView registerNib:[UINib nibWithNibName:#"nibname"
bundle:nil]
forCellReuseIdentifier:#"cellIdentifier"];
I've found a solution, albeit not a perfect one:
The AFNetworking library is brilliant and I assume the cause of my problem lies within my own code or my lack of understanding as to how NSCache works.
AFNetworking caches images using Apple's NSCache. NSCache is similar to NSMutableDictionary, but releases objects when memory is spread thin (see more here).
Within UIImageView+AFNetworking.m I located the definition of
+ (AFImageCache *)af_sharedImageCache
And altered it to resemble this:
+ (AFImageCache *)af_sharedImageCache {
static AFImageCache *_af_imageCache = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_af_imageCache = [[AFImageCache alloc] init];
_af_imageCache.countLimit = 35;
});
return _af_imageCache;
}
The important line here is
_af_imageCache.countLimit = 35;
This tells the NSCache object being used in AFNetworking to cache images that it must only cache up to a maximum of 35 things.
For reasons unknown to me, iOS was not automatically purging objects from the cache as it should, and calling removeAllObjects on the cache was not working either. This solution is hardly ideal because it may not utilize the cache to its full potential or may over use the cache, but for the meantime it atleast stops the cache from attempting to store an infinite number of objects.
I'm using a double for loop to add UIButtons to a UIScrollView in a grid format. These UIButtons take time to load as they have subviews that are UIImageViews which get their UIImages by downloading data off the internet.
Right now, the subviews don't show until AFTER the method completely finishes executing. Correct me if I'm wrong, but I'm guessing xcode doesn't show added subviews until a method is done executing.
However, I do want to show each subview getting added one at a time, as a cool loading effect. How would I implement this?
Thanks!
You should use multiple threads to load your pictures so that your main thread does not become sluggish. I recently wrote something similar...Take a look at my code from my viewWillAppear method:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.myImages = [self.myModel retrieveAttachments]; //Suppose this takes a long time
for (UIImage *image in self.myImages)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self addImageToScrollView:image animated:YES]; });
}
}
});
The addImageToScrollView method would be like so:
-(void) addImageToScrollView: (UIImage *) image animated: (BOOL) animated
{
//Create image view
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.image = image;
if(animated)
{
imageView.alpha = 0;
[self.myScrollView addSubview:imageView];
[UIView animateWithDuration:ADD_IMAGE_APPEARING_ANIMATION_SPEED animations:^{
imageView.alpha = 1;
}];
}
else
{
[self.myScrollView addSubview:imageView];
}
}