I have an app that creates an animation from images stored in a group in my project navigator (not Images.xcassets). This code "works" in that it animates properly, but using imageNamed causes a memory leak because the image files are not getting deallocated.
I can't figure out why adding with imageNamed: works adds images to my array, but imageWithContentsOfFile: doesn't.
A little info on the mechanics of my app:
self.myPhoto is set on the segue from another ViewController. The number of images can vary, so I test to see the file is "there" before adding it to the array.
Filenames follow this naming convention:
"1-1.jpg"
"2-1.jpg"
"2-2.jpg"
"99-1.jpg"
"99-2.jpg"
"99-3.jpg"
"99-4.jpg"
This code works, but the images don't deallocate, causing a memory leak:
- (void)startAnimation {
NSMutableArray *imageArray = [[NSMutableArray alloc] init];
for (int imageNumber = 1; self.myPhoto != nil; imageNumber++) {
NSString *fileName = [NSString stringWithFormat:#"%#-%d.jpg", self.myPhoto, imageNumber];
// check if a file exists
if ([UIImage imageNamed:fileName]) {
// if it exists, add it to the array
[imageArray addObject:[UIImage imageNamed:fileName]];
} else {
// otherwise, don't add image to the array
break;
}
}
self.myImageView.animationImages = imageArray;
self.myImageView.animationDuration = 1.5f;
self.myImageView.animationRepeatCount = 0;
self.myImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.myImageView startAnimating];
}
I ran Instruments on it and saw I had a memory leak emanating from my animation. Digging around a little on StackOverflow, I discovered the manner I'm adding my files to myArray results in images not getting deallocated.
So I tried this, instead:
- (void)startAnimation {
NSMutableArray *imageArray = [[NSMutableArray alloc] init];
for (int imageNumber = 1; self.myPhoto != nil; imageNumber++) {
NSString *fileName = [NSString stringWithFormat:#"%#-%d", self.myPhoto, imageNumber];
// check if a file exists
if ([UIImage imageNamed:fileName]) {
// if it exists, add it to the array
[imageArray addObject:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:[NSString stringWithFormat:#"%#", fileName] ofType:#"jpg"]]];
NSLog(#"%# added to imageArray", fileName);
} else {
// otherwise, don't add image to the array
break;
}
}
NSLog(#"There are %lu images in imageArray", (unsigned long)imageArray.count);
self.myImageView.animationImages = imageArray;
self.myImageView.animationDuration = 1.5f;
self.myImageView.animationRepeatCount = 0;
self.myImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.myImageView startAnimating];
}
When I do it this way, the page where the animation loads appears, but the images don't get added to my array--the . This is a well-documented issue. Here are a few posts covering this problem:
Dirty Memory because of CoreAnimation and CG image
How do I use imageWithContentsOfFile for an array of images used in an animation?
Thank you for reading. I'm stumped, though I'm confident the resolution to this problem is a startlingly stupid oversight on my part. Prove me right ;)
I made some minor changes out of desperation and I stumbled into the "answer". Comments note where I made changes:
- (void)startAnimation {
NSMutableArray *imageArray = [[NSMutableArray alloc] init];
for (int imageNumber = 1; self.myPhoto != nil; imageNumber++) {
// I set the filename here, adding .jpg to it
NSString *fileName = [NSString stringWithFormat:#"%#-%d.jpg", self.myPhoto, imageNumber];
// check if a file exists
if ([UIImage imageNamed:fileName]) {
// if it exists, add it to the array
// I blanked out ofType, as it's set when I create the local fileName variable
[imageArray addObject:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:[NSString stringWithFormat:#"%#", fileName] ofType:#""]]];
} else {
// otherwise, don't add image to the array
break;
}
}
self.myImageView.animationImages = imageArray;
self.myImageView.animationDuration = 1.5f;
self.myImageView.animationRepeatCount = 0;
self.myImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.myImageView startAnimating];
}
Related
I've created NSManagedObject* imagesArrayData that stores strings (paths) to images stored in the documents directory:
- (void)setImagesArray:(NSMutableArray *)imagesArray {
NSMutableArray* newImagesArray = [NSMutableArray new];
int i = 1;
for (UIImage* image in imagesArray) {
//generate path to createdFile
NSString* fileName = [NSString stringWithFormat:#"%#_%d", self.name, i];
NSString* filePath = [self documentsPathForFileName:fileName];
//save image to disk
NSData *imageData = UIImagePNGRepresentation(image);
[imageData writeToFile:filePath atomically:YES];
//add image path to CoreData
[newImagesArray addObject:filePath];
i++;
}
//set new value of imagesArray
imagesArrayData = [NSKeyedArchiver archivedDataWithRootObject:newImagesArray];
I am now not showing pathsToImages in header file, but property imagesArray:
-(NSMutableArray*) imagesArray {
NSMutableArray* images = [NSMutableArray new];
NSArray* imagePaths = [NSKeyedUnarchiver unarchiveObjectWithData:imagesArrayData];
for (NSString* imagePath in imagePaths) {
UIImage *image = [[UIImage alloc] initWithContentsOfFile: imagePath];
[images addObject:image];
}
return images;
The problem is, that whenever I want to get to [imagesArray objectatIndex:xxx], the imagesArray getter is called, and it takes time to recreate the full array. When trying to switch fast between images, the UI slows down.
What would be the elegant way to overcome this problem? Maybe creating another array full of images and updating it from time to time? Maybe something else? Please, help.
One thing you could do is refactor your getter to lazily load the array. If it is already defined, simply return it. If not, build it:
-(NSMutableArray*) imagesArray
{
if (!_imagesArray)
{
NSMutableArray* _imagesArray = [NSMutableArray new];
NSArray* imagePaths =
[NSKeyedUnarchiver unarchiveObjectWithData: imagesArrayData];
for (NSString* imagePath in imagePaths)
{
UIImage *image = [[UIImage alloc] initWithContentsOfFile: imagePath];
[_imagesArray addObject:image];
}
return _imagesArray;
}
I'm not sure what you mean about updating an array of images from time to time.
If your array of image names changes you will need some method to respond to those changes.
I am kinda new to Xcode and I am making a program. What I would like to do is when a person gets a score, it will output it on the screen. Example, the person got a score of 132, it would show the score as "132", how would I replace the numbers with an image so instead of using a font, the numbers are showing as an image. I was thinking if there is anyway to use something like this to output the numbers as a picture, but this wouldn't put them in order or it wouldn't work anyway. If anyone can help me, i'd really appreciate it. Thanks!
one = [UIImage imageNamed:#"1.png"]; //Image for the number 1
two = [UIImage imageNamed:#"2.png"]; //Image for the number 2
three = [UIImage imageNamed:#"3.png"]; //Image for the number 3
NSString *score = "132"; //This is the score the user got
if ([score rangeOfString:#"1"].location == NSFound) {
imagescore = one; //If the score contains one, the image adds the image 1
}
if ([score rangeOfString:#"2"].location == NSFound) {
imagescore += two; //If the score contains two, the image adds the image 2
}
if ([score rangeOfString:#"3"].location == NSFound) {
imagescore +=three; //If the score contains two, the image adds the image 3
}
imageView.image = imagescore; //Would change the image to the images of the score in order
Here i made an array of UIImage from your score string:
EDIT: Now i commented out the array making parts and included iMani's solution to merge the images together. Is it now clear?
-(void)calculateScore{
NSString *score = #"132";
UIImage *scroreImage = [UIImage new];
// NSMutableArray *images = [[NSMutableArray alloc]initWithCapacity:score.length];
for (int i = 0; i < score.length; i++) {
// imageName will be #"1", then #"3" and #"2"
NSString *imageName = [[score substringToIndex:i] substringToIndex:1];
// add extension
imageName = [imageName stringByAppendingString:#".png"];
UIImage *image = [UIImage imageNamed:imageName];
//[images addObject:image];
scroreImage = [self concateImageOne:scroreImage withImageTwo:image]
}
}
-(UIImage*)concateImageOne:(UIImage*)image1 withImageTwo:(UIImage*)image2
{
///Merge images together
}
concateImageOne:withImageTwo: selector made by iMani
I can understand little bit. You have number image like 1.png.,2.png,3.png. If score is 132, you want to show image as 1.png + 2.png + 3.png. That want merge this image into one image.
Code for Merge two images..
-(UIImage*)concateImageOne:(UIImage*)image1 withImageTwo:(UIImage*)image2
{
CGSize size = CGSizeMake(image1.size.width, image1.size.height + image2.size.height);
UIGraphicsBeginImageContext(size);
[image1 drawInRect:CGRectMake(0,0,size.width, image1.size.height)];
[image2 drawInRect:CGRectMake(0,image1.size.height,size.width, image2.size.height)];
UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return finalImage;
}
Now check with condition, and merge images.
if ([score rangeOfString:#"1"].location == NSFound) {
if (!imagescore)
imagescore = one; //If the score contains one, the image adds the image 1
else
imagescore = [self concateImageOne:imagescore withImageTwo:one]
}
As like, merge all image with condition. Finally, recreate imageview and assign image as below.
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imagescore.size.width, imagescore.size.height)];
imageView.image = imagescore;
You can get list of Image by follow code:
- (NSArray *)imageFromString:(NSString *)scoreString
{
NSMutableArray *scoreImages = [[NSMutableArray alloc] init];
for (int i = 0; i < scoreString.length; i++) {
NSString *imageName = [NSString stringWithFormat:#"%c.png", [scoreString characterAtIndex:i]];
UIImage *image = [UIImage imageNamed:imageName];
[scoreImages addObject:image];
}
return scoreImages;
}
and write a function to show this list image to view
What I did was:
-(void)calculateScore {
int hundred = self.score / 100;
int ten = self.score / 10;
int remainder = self.score % 10;
NSString *hundredString = [NSString stringWithFormat:#"number_%d.png", hundred];
NSString *tenString = [NSString stringWithFormat:#"number_%d.png", ten];
NSString *remainderString = [NSString stringWithFormat:#"number_%d.png", remainder];
self.hundredImageView.image = [UIImage imageNamed:hundredString];
self.tenImageView.image = [UIImage imageNamed:tenString];
self.remainderImageView.image = [UIImage imageNamed:remainderString];
}
I have a method which downloads some pics from a server. I had the buffer for the downloaded data defined withing async block (NSMutableArray *imgtmp) but haven't figured out how to get the array back out of there. What's the best way to access imgtmp in order to return it's contents, or set an instance variable from it?
I've been looking in the Apple Block docs but I must not be in the right section. Do I need to declare imgtmp using the __block keyword somehow? I tried that but imgtmp was still empty outside the block. Thanks!
EDIT: code updated with working model
- (void) loadImages
{
// temp array for downloaded images. If all downloads complete, load into the actual image data array for tablerows
__block NSMutableArray *imgtmp = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0),
^{
int error = 0;
int totalitems = 0;
NSMutableArray *picbuf = [[NSMutableArray alloc] init];
for (int i=0; i < _imageURLS.count;i++)
{
NSLog(#"loading image for main image holder at index %i",i);
NSURL *mynsurl = [[NSURL alloc] initWithString:[_imageURLS objectAtIndex:i]];
NSData *imgData = [NSData dataWithContentsOfURL:mynsurl];
UIImage *img = [UIImage imageWithData:imgData];
if (img)
{
[picbuf addObject:img];
totalitems++;
}
else
{
NSLog(#"error loading img from %#", [_imageURLS objectAtIndex:i]);
error++;
}
}// for int i...
dispatch_async(dispatch_get_main_queue(),
^{
NSLog(#"_loadedImages download COMPLETE");
imgtmp = picbuf;
[_tvStatus setText: [NSString stringWithFormat:#"%d objects have been retrieved", totalitems]];
NSLog (#"imgtmp contains %u images", [imgtmp count]);
});// get_main_queue
});// get_global_queue
}
You're hitting that "final" NSLog call before any of your block code has executed. All your image loading stuff is wrapped in a dispatch_async. It is executed asynchronously whereas the NSLog is called right away.
I think the best thing for you to do is to pass imgtmp along to some persistent object. Perhaps your view controller can have a property like:
#property (nonatomic, copy) NSArray *images;
and you can assign that in the same block where you assign the text to _tvStatus.
I have the following code:
NSMutableArray *imageArray = [[NSMutableArray alloc] init];
for(int i = 1; i <= 32; i++) {
//NSString *str = [NSString stringWithFormat:#"marker_%i.png", i];
NSString *str = [NSString stringWithFormat:#"wave%i.png", i];
UIImage *img = [UIImage imageNamed:str];
if(img != nil) {
[imageArray addObject:img];
}
}
NSLog(#"imageArray count %i",[imageArray count]);
NSMutableArray *reverseArray = [NSMutableArray arrayWithArray:imageArray];
[[reverseArray reverseObjectEnumerator] allObjects];
[imageArray addObjectsFromArray:reverseArray];
NSLog(#"imageArray count %i",[imageArray count]);
If i run it on the iphone i get:
2013-05-12 15:21:08.539 RouteApp[5673:907] imageArray count 23
2013-05-12 15:21:08.545 RouteApp[5673:907] imageArray count 46
while if i run in the simulator i get 32 and 64 like i expect.
(like if the numbers are switched)
What can cause this?
It looks like some of the images are missing on the iPhone, so this line
if(img != nil) {
[imageArray addObject:img];
}
filters them out. Change this line as follows to see which images are not present:
if(img != nil) {
[imageArray addObject:img];
} else {
NSLog("Image 'wave%i.png' is missing", i);
}
Filenames on the device are case-sensitive, but are not on the simulator. Some of your image names are likely slightly different from the format you're expecting.
The most common way this happens is something like wave01.png vs. wave01.PNG.
Otherwise it's probably something like wave01.png vs. Wave01.png, etc.
I have some code which randomly displays a given .png image and its title.The title logic is working fine. However, for some reason, only two of the four images display. I've tried changing the order just in case, but only those two images, pic_c and pic_d, ever get displayed. I've also checked the spelling, and that the files are in resources and exist on the file system. Here's the code which displays the image:
NSMutableArray *fileNameArray = [[NSMutableArray alloc] init];
[fileNameArray addObject:[NSString stringWithFormat: #"pic_a"]];
[fileNameArray addObject:[NSString stringWithFormat: #"pic_b"]];
[fileNameArray addObject:[NSString stringWithFormat: #"pic_c"]];
[fileNameArray addObject:[NSString stringWithFormat: #"pic_d"]];
NSMutableArray *titleArray = [[NSMutableArray alloc] init];
[titleArray addObject:[NSString stringWithFormat: #"pic a"]];
[titleArray addObject:[NSString stringWithFormat: #"pic b"]];
[titleArray addObject:[NSString stringWithFormat: #"pic c"]];
[titleArray addObject:[NSString stringWithFormat: #"pic d"]];
int index = arc4random() % [fileNameArray count];
NSString *pictureName = [titleArray objectAtIndex:index];
art_title.text = pictureName;
NSString* imagePath = [ [ NSBundle mainBundle] pathForResource:pictureName ofType:#"png"];
UIImage *img = [ UIImage imageWithContentsOfFile: imagePath];
if (img != nil) { // Image was loaded successfully.
[imageView setImage:img];
[imageView setUserInteractionEnabled:NO];
[img release]; // Release the image now that we have a UIImageView that contains it.
}
[super viewDidLoad];
[fileNameArray release];
[titleArray release];
}
Any idea why this might be happening?
Recent information: If I remove the titleArray, and use the file name array only, all the images show up. However, I'd still like to be able to use the titleArray.
What are your image files actually called? In the code above you don't actually do anything with finleNameArray except count it. You use the values from titleArray to get the image resource. I would think you need the following code in there:
NSString *filename = [fileNameArray objectAtIndex:index];
Then change your imagePath to use this instead of pictureName.