iOS UIImageView memory not getting deallocated on ARC - ios

I want to animate an image view in circular path and on click of image that image view need to change the new image. My problem is the images i allocated to the image view is not deallocated. And app receives memory warning and crashed. I surfed and tried lot of solutions for this problem but no use. In my case i need to create all ui components from Objective c class. Here am posting the code for creating image view and animation.
#autoreleasepool {
for(int i= 0 ; i < categories.count; i++)
{
NSString *categoryImage = [NSString stringWithFormat:#"%#_%ld.png",[categories objectAtIndex:i],(long)rating];
if (paginationClicked) {
if([selectedCategories containsObject:[categories objectAtIndex:i]]){
categoryImage = [NSString stringWithFormat:#"sel_%#",categoryImage];
}
}
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [self.mySprites objectForKey:categoryImage];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.clipsToBounds = NO;
[imageView sizeToFit];
imageView.accessibilityHint = [categories objectAtIndex:i];
// imageView.frame = CGRectMake(location.x+sin(M_PI/2.5)*(self.view.frame.size.width*1.5),location.y+cos(M_PI/2.5)*(self.view.frame.size.width*1.5) , 150, 150);
imageView.userInteractionEnabled = YES;
imageView.multipleTouchEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(categoryTapGestureCaptured:)];
singleTap.numberOfTapsRequired = 1;
[imageView addGestureRecognizer:singleTap];
[categoryView addSubview:imageView];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:location
radius:self.view.frame.size.width*1.5
startAngle:0.8
endAngle:-0.3+(0.1*(i+1))
clockwise:NO];
animation.path = path.CGPath;
imageView.center = path.currentPoint;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
animation.duration = 1+0.25*i;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Apply it
[imageView.layer addAnimation:animation forKey:#"animation.trash"];
}
}
And this is the code to change the image on click.
for (UIImageView *subview in subviews) {
NSString *key = [NSString stringWithFormat:#"%#_%ld.png",subview.accessibilityHint,(long)rating];
if ([SelectedCategory isEqualToString:subview.accessibilityHint]) {
NSString *tempSubCategory = [categoryObj objectForKey:SelectedCategory];
if([selectedCategories containsObject:SelectedCategory]){
subview.image = [self.mySprites objectForKey:key];
[selectedCategories removeObject:SelectedCategory];
if (tempSubCategory.length != 0) {
subCategoriesAvailable = subCategoriesAvailable-1;
}
[self showNoPagination:subCategoriesAvailable+2];
}else{
if(selectedCategories.count != 2){
key = [NSString stringWithFormat:#"sel_%#",key];
subview.image = [self.mySprites objectForKey:key];
[selectedCategories addObject:SelectedCategory];
if ([SelectedCategory isEqualToString:#"Other"]) {
[self showCommentDialog];
}else{
if (tempSubCategory.length != 0) {
subCategoriesAvailable = subCategoriesAvailable+1;
}
[self showNoPagination:subCategoriesAvailable+2];
}
}
}
[self disableCategories];
break;
}
}
And i don't know what am doing wrong here. I tried nullifying on for loop but no use.
Code which i used for removing the image view
UIView *categoryView = [self.view viewWithTag:500];
NSArray *subviews = [categoryView subviews];
for (UIImageView *subview in subviews) {
if(![selectedCategories containsObject:subview.accessibilityHint]){
[subview removeFromSuperview];
subview.image = Nil;
}
}
Adding sprite reader code for reference
#import "UIImage+Sprite.h"
#import "XMLReader.h"
#implementation UIImage (Sprite)
+ (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename
{
CGFloat scale = [UIScreen mainScreen].scale;
NSString* file = [filename stringByDeletingPathExtension];
if ([[UIScreen mainScreen] respondsToSelector:#selector(displayLinkWithTarget:selector:)] &&
(scale == 2.0))
{
file = [NSString stringWithFormat:#"%##2x", file];
}
NSString* extension = [filename pathExtension];
NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithFormat:#"%#.%#", file,extension]];
NSError* error = nil;
NSDictionary* xmlDictionary = [XMLReader dictionaryForXMLData:data error:&error];
NSDictionary* xmlTextureAtlas = [xmlDictionary objectForKey:#"TextureAtlas"];
UIImage* image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#.%#", file,[[xmlTextureAtlas objectForKey:#"imagePath"]pathExtension]]];
CGSize size = CGSizeMake([[xmlTextureAtlas objectForKey:#"width"] integerValue],
[[xmlTextureAtlas objectForKey:#"height"] integerValue]);
if (!image || CGSizeEqualToSize(size, CGSizeZero)) return nil;
CGImageRef spriteSheet = [image CGImage];
NSMutableDictionary* tempDictionary = [[NSMutableDictionary alloc] init];
NSArray* xmlSprites = [xmlTextureAtlas objectForKey:#"sprite"];
for (NSDictionary* xmlSprite in xmlSprites)
{
CGRect unscaledRect = CGRectMake([[xmlSprite objectForKey:#"x"] integerValue],
[[xmlSprite objectForKey:#"y"] integerValue],
[[xmlSprite objectForKey:#"w"] integerValue],
[[xmlSprite objectForKey:#"h"] integerValue]);
CGImageRef sprite = CGImageCreateWithImageInRect(spriteSheet, unscaledRect);
// If this is a #2x image it is twice as big as it should be.
// Take care to consider the scale factor here.
[tempDictionary setObject:[UIImage imageWithCGImage:sprite scale:scale orientation:UIImageOrientationUp] forKey:[xmlSprite objectForKey:#"n"]];
CGImageRelease(sprite);
}
return [NSDictionary dictionaryWithDictionary:tempDictionary];
}
#end
Please help me to resolve this. Thanks in advance.

It looks like all the images are being retained by the dictionary(assumption) self.mySprites, as you are loading them with the call imageView.image = [self.mySprites objectForKey:categoryImage];
If you loaded the images into the dictionary with +[UIImage imageNamed:], then the dictionary initially contains only the compressed png images. Images are decompressed from png to bitmap as they are rendered to the screen, and these decompressed images use a large amount of RAM (that's the memory usage you're seeing labeled "ImageIO_PNG_Data"). If the dictionary is retaining them, then the memory will grow every time you render a new one to the screen, as the decompressed data is held inside the UIImage object retained by the dictionary.
Options available to you:
Store the image names in the self.mySprites dictionary, and load the images on demand. You should be aware that +[UIImage imageNamed:] implements internal RAM caching to speed things up, so this might also cause memory issues for you if the images are big, as the cache doesn't clear quickly. If this is an issue, consider using +[UIImage imageWithContentsOfFile:], although it requires some additional code (not much), which doesn't cache images in RAM.
Re-implement self.mySprites as an NSCache. NSCache will start throwing things out when the memory pressure gets too high, so you'll need to handle the case that the image is not there when you expect it to be, and load it from disk (perhaps using the above techniques)

CAKeyframeAnimation inherits from CAPropertyAnimation which in tern is inherited from CAAnimation.
If you see the delegate of CAAnimation class, it is strongly referenced as written below as it is declared -
/* The delegate of the animation. This object is retained for the
* lifetime of the animation object. Defaults to nil. See below for the
* supported delegate methods. */
#property(strong) id delegate;
Now you have added the reference of animation on imageView.layer, doing so the reference of imageView.layer will be strongly retained by CAAnimation reference.
Also you have set
animation.removedOnCompletion = NO;
which won't remove the animation from layer on completion
So if you are done with a image view then first removeAllAnimations from its layer and then release the image view.
I think as the CAAnimation strongly refers the imageView reference(it would also have increased it's retain count) and this could be the reason that you have removed the imageView from superview, after which it's retain count would still not be zero, and so leading to a leak.

Is there any specific requirement to set
animation.removedOnCompletion = NO;
since, setting
animation.removedOnCompletion = YES;
could solve the issue related to memory leak.

Alternatively, to resolve memory leak issue you can remove the corresponding animation on corresponding imageView' layer, by implementing delegate of CAAnimation, like as shown below -
/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        NSLog(#"%#", #"The animation is finished. Do something here.");
    }
//Get the reference of the corresponding imageView
    [imageView.layer removeAnimationForKey:#"animation.trash"];
}
UIView *categoryView = [self.view viewWithTag:500];
NSArray *subviews = [categoryView subviews];
for (UIImageView *subview in subviews) {
if(![selectedCategories containsObject:subview.accessibilityHint]){
[subview.layer removeAnimationForKey:#"animation.trash"]; // either this
//or// [subview.layer removeAllAnimations]; //alternatively
[subview removeFromSuperview];
subview.image = Nil;
}
}

Related

UICollection View Scroll lag with SDWebImage

Background
I have searched around SO and apple forum. Quite a lot of people talked about performance of collection view cell with image. Most of them said it is lag on scroll since loading the image in the main thread.
By using SDWebImage, the images should be loading in separate thread. However, it is lag only in the landscape mode in the iPad simulator.
Problem description
In the portrait mode, the collection view load 3 cells for each row. And it has no lag or insignificant delay.
In the landscape mode, the collection view load 4 cells for each row. And it has obvious lag and drop in frame rate.
I have checked with instrument tools with the core animation. The frame rate drop to about 8fps when new cell appear. I am not sure which act bring me such a low performance for the collection view.
Hope there would be someone know the tricks part.
Here are the relate code
In The View Controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ProductCollectionViewCell *cell=[collectionView dequeueReusableCellWithReuseIdentifier:#"ProductViewCell" forIndexPath:indexPath];
Product *tmpProduct = (Product*)_ploader.loadedProduct[indexPath.row];
cell.product = tmpProduct;
if (cellShouldAnimate) {
cell.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
cell.alpha = 1.0;
} completion:nil];
}
if(indexPath.row >= _ploader.loadedProduct.count - ceil((LIMIT_COUNT * 0.3)))
{
[_ploader loadProductsWithCompleteBlock:^(NSError *error){
if (nil == error) {
cellShouldAnimate = NO;
[_collectionView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
cellShouldAnimate = YES;
});
} else if (error.code != 1){
#ifdef DEBUG_MODE
ULog(#"Error.des : %#", error.description);
#else
CustomAlertView *alertView = [[CustomAlertView alloc]
initWithTitle:#"Connection Error"
message:#"Please retry."
buttonTitles:#[#"OK"]];
[alertView show];
#endif
}
}];
}
return cell;
}
PrepareForReuse in the collectionViewCell
- (void)prepareForReuse
{
[super prepareForReuse];
CGRect bounds = self.bounds;
[_thumbnailImgView sd_cancelCurrentImageLoad];
CGFloat labelsTotalHeight = bounds.size.height - _thumbnailImgView.frame.size.height;
CGFloat brandToImageOffset = 2.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
brandToImageOffset = 53.0;
}
CGFloat labelStartY = _thumbnailImgView.frame.size.height + _thumbnailImgView.frame.origin.y + brandToImageOffset;
CGFloat nameLblHeight = labelsTotalHeight * 0.46;
CGFloat priceLblHeight = labelsTotalHeight * 0.18;
_brandLbl.frame = (CGRect){{15, labelStartY}, {bounds.size.width - 30, nameLblHeight}};
CGFloat priceToNameOffset = 8.0;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
priceToNameOffset = 18.0;
}
_priceLbl.frame = (CGRect){{5, labelStartY + nameLblHeight - priceToNameOffset}, {bounds.size.width-10, priceLblHeight}};
[_spinner stopAnimating];
[_spinner removeFromSuperview];
_spinner = nil;
}
Override the setProduct method
- (void)setProduct:(Product *)product
{
_product = product;
_spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_spinner.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[self addSubview:_spinner];
[_spinner startAnimating];
_spinner.hidesWhenStopped = YES;
// Add a spinner
__block UIActivityIndicatorView *tmpSpinner = _spinner;
__block UIImageView *tmpImgView = _thumbnailImgView;
ProductImage *thumbnailImage = _product.images[0];
[_thumbnailImgView sd_setImageWithURL:[NSURL URLWithString:thumbnailImage.mediumURL]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// dismiss the spinner
[tmpSpinner stopAnimating];
[tmpSpinner removeFromSuperview];
tmpSpinner = nil;
if (nil == error) {
// Resize the incoming images
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat imageHeight = image.size.height;
CGFloat imageWidth = image.size.width;
CGSize newSize = tmpImgView.bounds.size;
CGFloat scaleFactor = newSize.width / imageWidth;
newSize.height = imageHeight * scaleFactor;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *small = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(),^{
tmpImgView.image = small;
});
});
if (cacheType == SDImageCacheTypeNone) {
tmpImgView.alpha = 0.0;
[UIView animateWithDuration:0.2
delay:0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{
tmpImgView.alpha = 1.0;
} completion:nil];
}
} else {
// loading error
[tmpImgView setImage:[UIImage imageNamed:#"broken_image_small"]];
}
}];
_brandLbl.text = [_product.brand.name uppercaseString];
_nameLbl.text = _product.name;
[_nameLbl sizeToFit];
// Format the price
NSNumberFormatter * floatFormatter = [[NSNumberFormatter alloc] init];
[floatFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[floatFormatter setDecimalSeparator:#"."];
[floatFormatter setMaximumFractionDigits:2];
[floatFormatter setMinimumFractionDigits:0];
[floatFormatter setGroupingSeparator:#","];
_priceLbl.text = [NSString stringWithFormat:#"$%# USD", [floatFormatter stringFromNumber:_product.price]];
if (_product.salePrice.intValue > 0) {
NSString *rawStr = [NSString stringWithFormat:#"$%# $%# USD", [floatFormatter stringFromNumber:_product.price], [floatFormatter stringFromNumber:_product.salePrice]];
NSMutableAttributedString * string = [[NSMutableAttributedString alloc] initWithString:rawStr];
// Change all the text to red first
[string addAttribute:NSForegroundColorAttributeName
value:[UIColor colorWithRed:157/255.0 green:38/255.0 blue:29/255.0 alpha:1.0]
range:NSMakeRange(0,rawStr.length)];
// find the first space
NSRange firstSpace = [rawStr rangeOfString:#" "];
// Change from zero to space to gray color
[string addAttribute:NSForegroundColorAttributeName
value:_priceLbl.textColor
range:NSMakeRange(0, firstSpace.location)];
[string addAttribute:NSStrikethroughStyleAttributeName
value:#2
range:NSMakeRange(0, firstSpace.location)];
_priceLbl.attributedText = string;
}
}
SDWebImage is very admirable, but DLImageLoader is absolutely incredible, and a key piece of many big production apps
https://stackoverflow.com/a/19115912/294884
it's amazingly easy to use.
To avoid the skimming problem, basically just introduce a delay before bothering to start downloading the image. So, essentially like this...it's this simple
dispatch_after_secs_on_main(0.4, ^
{
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// so in other words, in fact, after a short period of time,
// the user has indeed scrolled away from that item.
// (ie, the user is skimming)
// this item is now some "new" item so of course we don't
// bother loading "that old" item
// ie, we now know the user was simply skimming over that item.
// (just TBC in the preliminary clause above,
// since the image is already in cache,
// we'd just instantly load the image - even if the user is skimming)
// NSLog(#" --- --- --- --- --- --- too quick!");
return;
}
// a short time has passed, and indeed this cell is still "that" item
// the user is NOT skimming, SO we start loading the image.
//NSLog(#" --- not too quick ");
[DLImageLoader loadImageFromURL:urlWasThen
completed:^(NSError *error, NSData *imgData)
{
if (self == nil) return;
// some time has passed while the image was loading from the internet...
if ( ! [urlWasThen isEqualToString:self.currentImage] )
{
// note that this is the "normal" situation where the user has
// moved on from the image, so no need toload.
//
// in other words: in this case, not due to skimming,
// but because SO much time has passed,
// the user has moved on to some other part of the table.
// we pointlessly loaded the image from the internet! doh!
//NSLog(#" === === 'too late!' image load!");
return;
}
UIImage *image = [UIImage imageWithData:imgData];
self.someImage.image = image;
}];
});
That's the "incredibly easy" solution.
IMO, after vast experimentation, it actually works considerably better than the more complex solution of tracking when the scroll is skimming.
once again, DLImageLoader makes all this extremely easy https://stackoverflow.com/a/19115912/294884
Note that the section of code above is just the "usual" way you load an image inside a cell.
Here's typical code that would do that:
-(void)imageIsNow:(NSString *)imUrl
{
// call this routine o "set the image" on this cell.
// note that "image is now" is a better name than "set the image"
// Don't forget that cells very rapidly change contents, due to
// the cell reuse paradigm on iOS.
// this cell is being told that, the image to be displayed is now this image
// being aware of scrolling/skimming issues, cache issues, etc,
// utilise this information to apprporiately load/whatever the image.
self.someImage.image = nil; // that's UIImageView
self.currentImage = imUrl; // you need that string property
[self loadImageInASecIfItsTheSameAs:imUrl];
}
-(void)loadImageInASecIfItsTheSameAs:(NSString *)urlWasThen
{
// (note - at this point here the image may already be available
// in cache. if so, just display it. I have omitted that
// code for simplicity here.)
// so, right here, "possibly load with delay" the image
// exactly as shown in the code above .....
dispatch_after_secs_on_main(0.4, ^
...etc....
...etc....
}
Again this is all easily possible due to DLImageLoader which is amazing. It is an amazingly solid library.

UIImageView uses a lot of memory

I'm creating some kind of image gallery. User can swipe between photo's. (Its just the concept, i know there are good library's out there that can handle this, but it's just to explain the concept).
I'm loading in 3 UIImageView, and i'm constantly reusing them. (Lazy loading). Problem is, when i change the image dynamiccaly from one imageview, and keep changing it, the memory is filling up. It's like it does not releases previous attached images. I'm using an array of UIImages that I already downloaded from some kind of web service.
As i load my view, i'm allocing and init my UIImageView, then I add it to the view.
UIImageView* _imgView;
NSArray* _imgArray;
So this array contains UIImage
-(void)changeImageIndex: (int) i
{
_imgView.image = [_imgArray objectAtIndex:i];
}
if i keep changing the imageindex, memory just gets filled up and filled up.. :s.
My project is ARC enabled.
Somebody has a clue how to solve it?
The image is added (actually changed) by this method:
-(void)setCurrentMovie:(GFilm*)film
{
_currentMovie = nil;
_currentMovie = film;
_posterView.image = nil;
[_youtubeView loadHTMLString:#"" baseURL:nil];
[_youtubeView stopLoading];
[_youtubeView setDelegate:nil];
[[NSURLCache sharedURLCache] removeAllCachedResponses];
[_scrollView setContentOffset:CGPointMake(0, 0) animated:NO];
CGFloat leftHeight = 0.f;
_leftView.frame = CGRectMake(10, 10, 110, 205);
leftHeight += _leftView.frame.size.height;
_posterView.frame = CGRectMake(0, 0, _leftView.frame.size.width, _leftView.frame.size.height - 40);
GImage* img = (GImage*)[_currentMovie.imageList objectAtIndex:0];
//_posterView.image = img.localImage;
//#autoreleasepool {
_posterView.image = [UIImage imageWithData:UIImagePNGRepresentation(img.localImage)];
_btnVertoningen.frame = CGRectMake(0, _posterView.frame.origin.y + _posterView.frame.size.height + 5, _leftView.frame.size.width, 35);
_posterView.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(posterClick)];
singleTap.numberOfTapsRequired = 1;
[_posterView addGestureRecognizer:singleTap];
[_leftView addSubview:_posterView];
CGFloat rightHeight = 0.f;
_rightView.frame = CGRectMake(_leftView.frame.origin.x + _leftView.frame.size.width + 5, _leftView.frame.origin.y, self.view.frame.size.width - (_leftView.frame.size.width + _leftView.frame.origin.x) - 15, 1);
_lblRegie.frame = CGRectMake(0, rightHeight, 48, 16);
NSMutableString* text = [[NSMutableString alloc]init];
NSArray* arr = _currentMovie.directorList;
etc.....
}
Probably your app have memory leak. Try use imageWithContentsOfFile instead of imageNamed:
_imgView.image = [UIImage imageWithContentsOfFile:imagePath];
Another possibility to have memory leaks with ARC is using a separate thread without an autorelease pool set up. Check this link:
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html:

Retrieve the name of the UIImage at a specific point in a UIScrollView

I'm having trouble getting my head around this; I've looked around for answers on here but either nothing directly applies to my question or I just can't make sense of it. (I am relatively new to this, so apologise if there is an obvious answer.)
I am inserting an array of UIImages (contained within a UIImageView) into a UIScrollView. I can programmatically scroll to points in the ScrollView, but I need to be able to identify by name which image is currently being shown after scrolling (so I can compare the image to one in another ScrollView).
How I have created my arrays and added the images to the ImageView and ScrollView is below.
ViewController.m
-(void)viewDidLoad {
...
// Store the names as strings
stringArr = [[NSMutableArray arrayWithObjects:
#"img0",
#"img1",
#"img2",
#"img3",
nil] retain];
// Add images to array
dataArr = [[NSMutableArray arrayWithObjects:
[UIImage imageNamed:[stringArr objectAtIndex:0]],
[UIImage imageNamed:[stringArr objectAtIndex:1]],
[UIImage imageNamed:[stringArr objectAtIndex:2]],
[UIImage imageNamed:[stringArr objectAtIndex:3]],
nil] retain];
// Use a dictionary to try and make it possible to retrieve an image by name
dataDictionary = [NSMutableDictionary dictionaryWithObjects:dataArr forKeys:stringArr];
i = 0;
currentY = 0.0f;
// Set up contents of scrollview
// I'm adding each of the four images four times, in a random order
for (imageCount = 0; imageCount < 4; imageCount++) {
// Add images from the array to image views inside the scroll view.
for (UIImage *image in reelDictionary)
{
int rand = arc4random_uniform(4);
UIImage *images = [dataArr objectAtIndex:rand];
imgView = [[UIImageView alloc] initWithImage:images];
imgView.contentMode = UIViewContentModeScaleAspectFit;
imgView.clipsToBounds = YES;
// I have tried to use this to tag each individual image
imgView.tag = i;
i++;
CGRect rect = imgView.frame;
rect.origin.y = currentY;
imgView.frame = rect;
currentY += imgView.frame.size.height;
[scrollReel1 addSubview:reel1_imgView];
[reel1_imgView release];
}
}
scrollReel.contentSize = CGSizeMake(100, currentY);
[self.view addSubview:scrollReel];
...
}
This is how I am working out where I am in the ScrollView (currentOffset), and also exactly which image I need to retrieve (symbolNo). The value of symbolNo is correct when I test it, but I am unsure how to use the value with respect to image name retrieval.
NSInteger currentOffset = scrollReel.contentOffset.y;
NSInteger symbolNo = (currentOffset / 100) + 1;
Thanks in advance for any assistance.
There is no way to do this. The UIImage object doesn't store its name once it's loaded.
You could get around this by using the tag property on the image views if all your images have numerical names.
Otherwise you'll need to find a new way to model your data.
You basically need the reverse mapping of what you had. Here is a quick and dirty solution
NSMutableDictionary *indexToImageMap = [NSMutableDictionary new];
for (imageCount = 0; imageCount < 4; imageCount++) {
// Add images from the array to image views inside the scroll view.
for (UIImage *image in reelDictionary)
{
int rand = arc4random_uniform(4);
UIImage *images = [dataArr objectAtIndex:rand];
imgView = [[UIImageView alloc] initWithImage:images];
imgView.contentMode = UIViewContentModeScaleAspectFit;
imgView.clipsToBounds = YES;
// I have tried to use this to tag each individual image
imgView.tag = i;
i++;
[indexToImageMap setObject:imgView forKey:[NSNumber numberWithInt:i];
CGRect rect = imgView.frame;
rect.origin.y = currentY;
imgView.frame = rect;
currentY += imgView.frame.size.height;
[scrollReel1 addSubview:reel1_imgView];
[reel1_imgView release];
}
}
And to look it up you do
NSInteger currentOffset = scrollReel.contentOffset.y;
NSInteger symbolNo = (currentOffset / 100) + 1;
NSImage *image = [indexToImageMap objectForKey:[NSNumber numberWithInt:symbolNo]];
Subclass image view and add imageName property. if i understand what you are asking this should work.
#import <UIKit/UIKit.h>
#interface myImageView : UIImageView
{
__strong NSString *imageName;
}
#property (strong) NSString *imageName;
#end
#import "myImageView.h"
#implementation myImageView
#synthesize imageName;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
#end
then use a dictionary to keep everything instead of array + dictionary.
myImageView *imgView1 = [[myImageView alloc] init];
[imgView1 setImageName:#"image_name_here"];
[imgView1 setImage:[UIImage imageNamed:#"image_name_here"]];
NSMutableDictionary *dicti = [[NSMutableDictionary alloc] init];
[dicti setObject:imgView1 forKey:#"image_name_here_1"];
[dicti setObject:imgView2 forKey:#"image_name_here_2"];
[dicti setObject:imgView... forKey:#"image_name_here_..."];
when you find the imageView you can search image in dictionary. because you know name of the imageView now.

CATiledLayer subview causing memory warning and crash [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 5 years ago.
Improve this question
Alright, here's what's happening... I have a UIView subclass, which includes two points (start and end), a CGPath between those two points and lot of UILabels. If I add that UIView subclass as a subview to my scrollview (over top of my CATiledLayer based UIView) then my app starts getting memory warnings and eventually crashes. However, if I remove the UIView subclass that contains the points and path and leave the CATiledLayer view as it is, everything functions perfectly and no crashing or memory warnings occur.
Does anyone have any ideas on why this is occurring? The content view shouldn't have any problems being so big should it since I'm only draw labels that are at max 40px wide and a CGPath, which is also pretty small?
This is a mapping application
Here's some code:
//Display the map image
- (void)displayTiledImageNamed:(NSString *)imageName size:(CGSize)imageSize {
self.zoomScale = 1.0;
container = [[UIView alloc] initWithFrame:(CGRect){ CGPointZero, imageSize }];
_zoomView = [[UIImageView alloc] initWithFrame:(CGRect){ CGPointZero, imageSize }];
NSString *path = [NSString stringWithFormat:#"%#/%#-Small.png",[[NSBundle mainBundle] bundlePath], [imageName stringByDeletingPathExtension]];
UIImage *zoomImage = [UIImage imageWithContentsOfFile:path];
[_zoomView setImage:zoomImage];
[container addSubview:_zoomView];
[_zoomView release];
_tilingView = [[UMTileView alloc] initWithImageName:imageName size:imageSize];
_tilingView.frame = _zoomView.bounds;
[_zoomView addSubview:_tilingView];
[_tilingView release];
[self configureForImageSize:imageSize];
//This is the view that's causing the crash and memory warnings
//If this view is commented out the app functions just fine
UMMapContentView *m = [[UMMapContentView alloc] initWithFrame:CGRectMake(0,0,imageSize.width,imageSize.height) andColors:self.colors mapView:self.mapView];
self.contentView = m;
[container addSubview:self.contentView];
[m release];
[self addSubview:container];
[container release];
}
This my path view class which is another subview inside the UMMapContentView view above:
+ (Class)layerClass {
return [CATiledLayer class];
}
+ (CFTimeInterval)fadeDuration {
return 0.0;
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)dealloc {
self.path = nil;
self.pathColor = nil;
[super dealloc];
}
- (void)resetPathView {
CATiledLayer *tiledLayer = (CATiledLayer*)self.layer;
tiledLayer.contents = nil;
[tiledLayer setNeedsDisplay];
}
- (void)drawPath:(UMPath*)p {
self.path = p;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
if (self.path) {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, self.pathColor.CGColor);
CGContextSetShadowWithColor(context, CGSizeMake(2.0, 1.0), 1.0, [[UIColor colorWithWhite:0.2 alpha:0.8] CGColor]);
CGContextSetLineWidth(context, 8.0);
CGContextBeginPath (context);
CGContextMoveToPoint (context, ((UMMapPoint*)[self.path.points objectAtIndex: 0]).x, ((UMMapPoint*)[self.path.points objectAtIndex: 0]).y);
for (int i = 1; i < self.path.points.count; i++) {
CGContextAddQuadCurveToPoint (context, ((UMMapPoint*)[self.path.points objectAtIndex:i-1]).x, ((UMMapPoint*)[self.path.points objectAtIndex: i-1]).y, ((UMMapPoint*)[self.path.points objectAtIndex:i]).x, ((UMMapPoint*)[self.path.points objectAtIndex:i]).y);
}
CGContextStrokePath (context);
}
}
And this is my method that places all the UILabels in the UMMapContentView view:
- (void)mapAllLabelsInNavigationMap:(int)navigationMap {
//There are 1109 label dictionaries in the labels array
for (NSDictionary *dict in labels) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake([[dict objectForKey:#"X"] floatValue],[[dict objectForKey:#"Y"] floatValue],[[dict objectForKey:#"Width"] floatValue],[[dict objectForKey:#"Height"] floatValue])];
[label setLineBreakMode:NSLineBreakByWordWrapping];
[label setNumberOfLines:0];
[label setBackgroundColor:[UIColor clearColor]];
[label setTextColor:[self colorFromHexString:[dict objectForKey:#"Foreground"]]];
[label setFont:[UIFont systemFontOfSize:[[dict objectForKey:#"FontSize"] floatValue]]];
[label setTextAlignment:NSTextAlignmentCenter];
[label setText:[[UMDataBase shared] stringForID:[[dict objectForKey:#"Texts_ID"] intValue] withLanguage:languageID]];
[self addSubview:label];
[localMapLabels addObject:label];
[label release];
}
}
I'm not really sure what direction I need to head... Any help would be much appreciated! :)
You did not give the source code of UMMapContentView, but if it uses CATiledLayer, then that may be the problem. It seems that you cannot subview a CATiledLayer-backed view.
Scroll to the bottom (in the comments section): http://red-glasses.com/index.php/tutorials/catiledlayer-how-to-use-it-how-it-works-what-it-does/
It may be easier if you write your own tiled layer anyhow. Documentation on CATiledLayer is sparse while the bugs are aplenty.

How to render a CALayer in the background

I need to save screenshots from my app, so I've set up code like this, which works:
- (void)renderScreen {
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CGSize outputSize = keyWindow.bounds.size;
UIGraphicsBeginImageContext(outputSize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CALayer *layer = [keyWindow layer];
[layer renderInContext:context];
CGContextRestoreGState(context);
UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// now save the screen image, etc...
}
However, when the screen image becomes complex (lots of views), the renderInContext can take up to 0.8 seconds on an iPad 3, and the user interface locks up during that time, which interferes with some other functionality. So I moved the rendering to a background thread, like this:
- (void)renderScreen {
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CALayer *layer = [keyWindow layer];
[self performSelectorInBackground:#selector(renderLayer:) withObject:layer];
}
- (void)renderLayer:(CALayer *)layer {
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CGSize outputSize = keyWindow.bounds.size;
UIGraphicsBeginImageContext(outputSize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
[layer renderInContext:context];
CGContextRestoreGState(context);
UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// now save the screen image, etc...
}
That allows the interface to run smoothly again, but occasionally causes a crash with EXC_BAD_ACCESS on the renderInContext line. I tried checking for layer!=nil and [layer respondsToSelector:#selector(renderInContext:)] first, so I could avoid the crash, but both conditions always return true.
Then I read this SO comment, stating that a layer could mutate before the background operation runs and suggesting sending a copy of the layer to the background operation instead. This SO answer and this one got me started, and I ended up with this category to add a copy method to CALayer:
#import "QuartzCore/CALayer.h"
#interface CALayer (CALayerCopyable)
- (id)copy;
#end
#implementation CALayer (CALayerCopyable)
- (id)copy {
CALayer *newLayer = [CALayer layer];
newLayer.actions = [self.actions copy];
newLayer.anchorPoint = self.anchorPoint;
newLayer.anchorPointZ = self.anchorPointZ;
newLayer.backgroundColor = self.backgroundColor;
//newLayer.backgroundFilters = [self.backgroundFilters copy]; // iOS 5+
newLayer.borderColor = self.borderColor;
newLayer.borderWidth = self.borderWidth;
newLayer.bounds = self.bounds;
//newLayer.compositingFilter = self.compositingFilter; // iOS 5+
newLayer.contents = [self.contents copy];
newLayer.contentsCenter = self.contentsCenter;
newLayer.contentsGravity = [self.contentsGravity copy];
newLayer.contentsRect = self.contentsRect;
//newLayer.contentsScale = self.contentsScale; // iOS 4+
newLayer.cornerRadius = self.cornerRadius;
newLayer.delegate = self.delegate;
newLayer.doubleSided = self.doubleSided;
newLayer.edgeAntialiasingMask = self.edgeAntialiasingMask;
//newLayer.filters = [self.filters copy]; // iOS 5+
newLayer.frame = self.frame;
newLayer.geometryFlipped = self.geometryFlipped;
newLayer.hidden = self.hidden;
newLayer.magnificationFilter = [self.magnificationFilter copy];
newLayer.mask = [self.mask copy]; // property is another CALayer
newLayer.masksToBounds = self.masksToBounds;
newLayer.minificationFilter = [self.minificationFilter copy];
newLayer.minificationFilterBias = self.minificationFilterBias;
newLayer.name = [self.name copy];
newLayer.needsDisplayOnBoundsChange = self.needsDisplayOnBoundsChange;
newLayer.opacity = self.opacity;
newLayer.opaque = self.opaque;
newLayer.position = self.position;
newLayer.rasterizationScale = self.rasterizationScale;
newLayer.shadowColor = self.shadowColor;
newLayer.shadowOffset = self.shadowOffset;
newLayer.shadowOpacity = self.shadowOpacity;
newLayer.shadowPath = self.shadowPath;
newLayer.shadowRadius = self.shadowRadius;
newLayer.shouldRasterize = self.shouldRasterize;
newLayer.style = [self.style copy];
//newLayer.sublayers = [self.sublayers copy]; // this line makes the screen go blank
newLayer.sublayerTransform = self.sublayerTransform;
//newLayer.superlayer = self.superlayer; // read-only
newLayer.transform = self.transform;
//newLayer.visibleRect = self.visibleRect; // read-only
newLayer.zPosition = self.zPosition;
return newLayer;
}
#end
Then I updated renderScreen to send a copy of the layer to renderLayer:
- (void)renderScreen {
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CALayer *layer = [keyWindow layer];
CALayer *layerCopy = [layer copy];
[self performSelectorInBackground:#selector(renderLayer:) withObject:layerCopy];
}
When I run this code, all the screen images are plain white. Obviously my copy method is not correct. So can someone help me with any of the following possible solutions?
How to write a copy method for CALayer that really works?
How to check that a layer passed into a background process is a valid target for renderInContext?
Any other way to render complex layers without locking up the interface?
UPDATE: I rewrote my CALayerCopyable category based on Rob Napier's suggestion to use initWithLayer. Simply copying the layer still gave me a plain white output, so I added a method to recursively copy all the sublayers. I still, however, get the plain white output:
#import "QuartzCore/CALayer.h"
#interface CALayer (CALayerCopyable)
- (id)copy;
- (NSArray *)copySublayers:(NSArray *)sublayers;
#end
#implementation CALayer (CALayerCopyable)
- (id)copy {
CALayer *newLayer = [[CALayer alloc] initWithLayer:self];
newLayer.sublayers = [self copySublayers:self.sublayers];
return newLayer;
}
- (NSArray *)copySublayers:(NSArray *)sublayers {
NSMutableArray *newSublayers = [NSMutableArray arrayWithCapacity:[sublayers count]];
for (CALayer *sublayer in sublayers) {
[newSublayers addObject:[sublayer copy]];
}
return [NSArray arrayWithArray:newSublayers];
}
#end
For this purpose, I'd use initWithLayer: rather than creating your own copy method. initWithLayer: is explicitly for creating "shadow copies of layers, for example, for the presentationLayer method."
You may also need to create copies of the sublayers. I can't remember immediately whether initWithLayer: does that for you. But initWithLayer: is how Core Animation works, so it's optimized for problems like this.

Resources