i'm trying to create a photo viewer like the Apple Photos app in iOS.
The layout is ok, but it receives memory warning and then crashes. Why? This happens even i load 7/8 images from the app documents folder. Have i to manage the memory with specific system? I use ARC with iOS 5.
EDIT :
The code :
for (int i=0; i<[dataSource count]; i++) {
UIButton *button=[UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:[dataSource objectAtIndex:i] forState:UIControlStateNormal];
[[button titleLabel] setText:[NSString stringWithFormat:#"%i",i+1]];
[button addTarget:self action:#selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
[[button layer] setBorderWidth:1];
[[button layer] setBorderColor:[UIColor darkGrayColor].CGColor];
if (i==0) {
[button setFrame:CGRectMake(x, y, width, height)];
} else {
if (i%5==0) {
nRow++;
x=18;
[button setFrame:CGRectMake(x, (y*nRow), width, height)];
} else {
[button setFrame:CGRectMake(x+space+width, (y*nRow), width, height)];
x=button.frame.origin.x;
}
}
[[self view] addSubview:button];
}
The main part of this code is the first 6 lines, after is all x and y.
dataSource is an NSArray declared as property (nonatomic, strong). It contains UIImage objects.
You should be lazily loading your images in conjunction with reusing your buttons to account for the possibility of a large number of images.
To implement:
Keep the paths to the image files in your data array instead of the UIImage objects. Get the image from the path using imageWithContentsOfFile: when you need it.
Load the first z buttons into the scroll view where z is the number that appear on the screen at a time plus one row's worth.
Set the UIViewController that we are currently in as the scrollview's delegate, and respond to changes in offset by repositioning buttons and setting appropriate images and targets.
Also, if 7/8 images is crashing your app, it sounds like you're dealing with some pretty large image files. Try to provide thumbnail-sized versions of the content within the documents directory(whatever the size of your buttons are EXACTLY), or if images are dynamic, see this post for a how-to.
If you are probably using ImageNamed, this article helped me alot:
http://www.alexcurylo.com/blog/2009/01/13/imagenamed-is-evil/
Mainly
DO NOT USE [UIImage imageNamed] for any significant amount of images. It is EVIL. It WILL bring down your application and/or Springboard, even when your application is putting along using just barely a nibble of memory on its own.
and
it is better to implement your own cache
and here's the proposed cached image example:
- (UIImage*)thumbnailImage:(NSString*)fileName
{
UIImage *thumbnail = [thumbnailCache objectForKey:fileName];
if (nil == thumbnail)
{
NSString *thumbnailFile = [NSString stringWithFormat:#"%#/thumbnails/%#.jpg", [[NSBundle mainBundle] resourcePath], fileName];
thumbnail = [UIImage imageWithContentsOfFile:thumbnailFile];
[thumbnailCache setObject:thumbnail forKey:fileName];
}
return thumbnail;
}
Related
I have the smooth scrolling issue at my UITableView with UITableViewCell which contains UIImageView. Similar issues could be found all over the StrackOverflow but none of the proposed solutions helped me to completely get rid of the lag.
My case is quite common:
images are stored at application storage (in my sample at app bundle)
images could have different size (500x500, 1000x1000, 1500x1500)
I need to display those images in UITableView where UIImageView size is 120x120 (retina)
I have followed multiple optimization tips and managed to optimize scrolling a lot.
Unfortunately it is still not perfect. This is my scenario:
first I moved all the image loading/processing/resizing logic to the background thread
UITableViewCell reuse is enabled
once UITableViewCell is in view I clear old values (settings to null) and start background thread to load the image
at this point we are in background thread and I'm adding 500 ms delay to avoid settings new image to often (in case we are scrolling fast) (see below explanation)
if UIImage exists at static image cache (regular dictionary with UIImage instances) - fetch that one and go to the step 9.
if not - load new image from bundle (imageWithName) using url to app bundle (in real world scenario images will be stored to application storage, not bundle)
once image is loaded resize it to 120x120 using graphics context
save resized image to the static image cache
at this point we have instance to UIImage and process is in the background thread. From here we move back to UI Thread with the given image
if data context was cleared (for example UITableViewCell disappeared or was reused to display another image) we skip processing of the currently available image.
if data context is the same - assign UIImage to UIImageView with an alpha animation (UIView.Animate)
once UITableViewCell is out of view - clear the data context
Originally before starting new background thread to fetch the image here (step 1) was UIImage cache check without background thread. In this case if we have the image in the cache we assign it instantly and this introduces a great lag during fast scrolling (we assign images to often as long as we fetch them instantly). Those lines are commented at my example attached below.
There are still two issues:
at some point during scrolling I still have a small lag (at the
moment when I'm assign new UIImage to UIImageView.
(this one is more noticeable) when you tap on item and go back from details there is a lag right before back navigation animation is finished.
Any suggest how to deal with those two issues or how to optimize my scenario are appreciated
Please take into account that sample written in Xamarin but I don't believe that Xamarin is the cause of the problem as long as I have the same issue for the app written in ObjectiveC as well.
Smooth Scrolling Test App
Did you every tried to populate your TableView with only one 120x120 Image which is saved in your Bundle? This way you can check, if the problem occurs of your Image rendering
Instead of resizing all your images to 120x120 and save them in cache, I would recommend creating and using a thumbnail of all your images. You are somehow already doing this, but you are doing this couple of times (everytime you are scrolling or if your cache is full).
In our last project we had a UICollectionView with book covers. Most of the covers were between 400-800kb big and the feeling while scrolling was really bad. So we created a thumbnail for each image (thumbails about 40-50kb) and used the thumbnails instead of real covers. Works like a charm! I attached the thumbnail creation function
- (BOOL) createThumbnailForImageAtFilePath:(NSString *)sourcePath withName:(NSString *)name {
UIImage* sourceImage = [UIImage imageWithContentsOfFile:sourcePath];
if (!sourceImage) {
//...
return NO;
}
CGSize thumbnailSize = CGSizeMake(128,198);
float imgAspectRatio = sourceImage.size.height / sourceImage.size.width;
float thumbnailAspectRatio = thumbnailSize.height/thumbnailSize.width;
CGSize scaledSize = thumbnailSize;
if(imgAspectRatio >= thumbnailAspectRatio){
//image is higher than thumbnail
scaledSize.width = scaledSize.height * thumbnailSize.width / thumbnailSize.height;
}
else{
//image is broader than thumbnail
scaledSize.height = scaledSize.width * imgAspectRatio;
}
UIGraphicsBeginImageContextWithOptions( scaledSize, NO, 0.0 );
CGRect scaledImageRect = CGRectMake( 0.0, 0.0, scaledSize.width, scaledSize.height );
[sourceImage drawInRect:scaledImageRect];
UIImage* destImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSString* thumbnailFilePath = [[self SOMEDIRECTORY] stringByAppendingPathComponent:name];
BOOL success = [UIImageJPEGRepresentation(destImage, 0.9) writeToFile:thumbnailFilePath atomically:NO];
return success;
}
Try facebook's Async Display library.
https://github.com/facebook/AsyncDisplayKit
Really easy to use.. from their guide: http://asyncdisplaykit.org/guide/
_imageNode = [[ASImageNode alloc] init];
_imageNode.backgroundColor = [UIColor lightGrayColor];
_imageNode.image = [UIImage imageNamed:#"hello"];
_imageNode.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageNode.view];
This decodes the image on a background thread.
I'm not sure if it's easy to use iOS libraries on Xamarin but if it's easy, give this a shot.
I sub-class Paul Hegarty's CoreDataTableViewController and employ thumbnails of my photos in the CoreDataTableView.
Look for the examples in Lecture 14 titled FlickrFetcher and Photomania. You will also need to download the CoreDataTableViewController at that same link.
Make a CoreData Entity with an appropriate title and define whatever attributes (data variables) you want. You will need to define two "Transformable" attributes, one for the photo and one for the thumbnail.
Then load your thumbnail in the CoreDataTableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *exceptions = [NSArray arrayWithObjects:#"SCR", #"DNS", #"NT", #"ND", #"NH", nil];
static NSString *CellIdentifier = #"resultsDisplayCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
MarksFromMeets *athleteMarks = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString* date = [ITrackHelperMethods dateToAbbreviatedString:athleteMarks.meetDate];
NSMutableString *title = [NSMutableString stringWithFormat:#"%#", athleteMarks.markInEvent];
NSMutableString *subTitle = [NSMutableString stringWithFormat:#"%# - %#",date, athleteMarks.meetName];
[title replaceOccurrencesOfString:#"(null)"
withString:#""
options:0
range:NSMakeRange(0, [title length])];
// cell.imageView.image = athleteMarks.photoThumbNail; // Don't like image in front of record.
[cell.textLabel setFont:[UIFont
fontWithName:#"Helvetica Neue" size:18]];
[cell.detailTextLabel setFont:[UIFont fontWithName:#"Helvetica Neue" size:16]];
[cell.detailTextLabel setTextColor:[UIColor grayColor]];
// make selected items orange
if ([athleteMarks.eventPR integerValue] != 0
&& (![exceptions containsObject:athleteMarks.markInEvent])) {
title = [NSMutableString stringWithFormat:#"%# (PR)",title];
[cell.textLabel setTextColor:[UIColor redColor]];
}
else if ([athleteMarks.eventSB integerValue] != 0
&& (![exceptions containsObject:athleteMarks.markInEvent])) {
title = [NSMutableString stringWithFormat:#"%# (SB)",title];
[cell.textLabel setTextColor:[UIColor orangeColor]];
} else {
[cell.textLabel setTextColor:[UIColor grayColor]];
}
cell.textLabel.text = title;
cell.detailTextLabel.text = subTitle;
cell.indentationLevel = indentationLevelOne;
cell.indentationWidth = indentationForCell;
return cell;
}
If you want, I can send you an example of a Category for an Entity's NSManagedObject Sub-Class. This Category loads the photo and the thumbnail into CoreData Entity. The first time will be slow. However, after that the user should be able to scroll through TableView smoothly and then all the updated results will load automatically. Let me know.
One nice thing is that CoreData handles all the memory management.
Good luck!
I don't have enough rep to comment, So here's an answer which helped my tableview scrolling performance:
Make the tableview height larger than the viewable window. Cells will load "off screen" and helps improve scroll smoothness.
Do your image processing in the following method:
-(void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Those two tricks got my table flowing really nice. I'm getting my image data from an API service and AFNETWORKING has an awesome image loader, but not necessary for you since images are in the bundle.
Maybe you could try SDWebImage instead. It is also a xamarin component
which fashions an asynchronous image downloader and asynchronous memory and disk image caching with automatic cache expiration handling. Using it would probably mean throwing away a lot of hard written code, but it might be worth it -plus your code will become a lot simpler. In iOS you can also setup a SDWebImageManager inside the viewDidLoad of a controller:
- (void)viewDidLoad{
...
SDWebImageManager *manager = [SDWebImageManager sharedManager];
manager.delegate = self;
...
}
and set the view controller as the delegate. Then, when the following delegate method is called:
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL
you could scale your images to thumbs of the appropriate size before caching them.
Hope that helps.
Weel I had a similar problem, my scroll was not smooth. I am inserting in the table a variable UIImageView with inside labelViews.
What I did was to change the method HeightforRowAtIndexPath for estimatedHeightforRowAtIndexPath and now scroll is smooth.
My app presents a scrolling list of buttons with image icons. I've found that even though the .png icon files are only 2 kb, their uncompressed versions are about 150 kb and the combined total of all the buttons is using too much device memory.
Originally each button was created within an instance of an object, so the code basically worked like this:
for (int i=0; i<buttonsCount; i++) {
UIImage *image = [UIImage imageNamed:#"Icon"];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:image forState:UIControlStateNormal];
}
When I realized how much memory the images were using, I moved the image creation outside of the object and let all the objects use the same image. So now the code basically works like this:
UIImage *image = [UIImage imageNamed:#"Icon"];
for (int i=0; i<buttonsCount; i++) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:image forState:UIControlStateNormal];
}
However, the memory usage is the same. Apparently UIButton is making its own copy of the image data. Is there any way to "share" the image data among many buttons?
When I run the app in Instruments using the Allocations tool, "VM: CG raster data" is the item that gets too large.
The only alternate solutions I can think of would be loading and unloading the buttons as needed while scrolling, but I'm afraid that would hurt the scrolling performance; or simply reducing the pixel size of the images.
You could use an approach similar to UITableView (or maybe use a UITableView directly). It only creates views (cells) for the items that are on screen, and then recycles these cells as they move offscreen. This means those views are not recreated all the time, they are just updated with the correct data just before they move on screen.
I am trying to create a button represented by an image which whenever is pressed changes the image to the other one so that I can know which image is currently selected.
- (IBAction)imageWasPressed:(id)sender {
UIImage *imageWork = [UIImage imageNamed:#"icn_work"];
NSData *data1 = UIImagePNGRepresentation(self.imageButton.currentImage);
NSData *data2 = UIImagePNGRepresentation(imageWork);
if (data1==data2){
[self.imageButton setImage:[UIImage imageNamed:#"icn_personal"] forState:UIControlStateNormal];}
}
I have also tried this but it didn't work:
- (IBAction)imageWasPressed:(id)sender {
if ([[self.imageButton imageForState:UIControlStateNormal] isEqual:[UIImage imageNamed:#"icn_work"]]){
[self.imageButton setImage:[UIImage imageNamed:#"icn_personal"] forState:UIControlStateNormal];}
}
The line which changes the images works but I can't compare the two images. Any help would be much appreciated! Thank you!
This is rather unconventional approach, and I would recommend keeping track of your selection some other way.
But to answer the issue at hand, it does not work because you are comparing pointers, not data.
data1 * will always be different to data2 *.
From the documentation:
isEqualToData:
Compares the receiving data object to otherData.
- (BOOL)isEqualToData:(NSData *)otherData
If you want to change button image regarding it's state you can assign different images for different states.
[self.imageButton setImage:image1 forState:UIControlStateNormal];
[self.imageButton setImage:image2 forState:UIControlStateHighlighted];
[self.imageButton setImage:image3 forState:UIControlStateSelected];
Since you can get button state and you know what image you set for the state you can get image as well.
if ([data1 isEqualToData: data2])
instead of
if (data1==data2)
how can i create a UIButton in shape of the image given.
i am able to create something close to it,but the image seems to be not fitting in the button.
my requirement is the image given below.i.e;semicircle
the code i used to create the button is given below.
what changes should i make on the following image to get this button.
ps-add subview done in another class..
btnStartGame =[UIButton buttonWithType:UIButtonTypeCustom];
[btnStartGame setFrame:CGRectMake(60, 200, 200, 200)];
btnStartGame.titleLabel.font=[UIFont fontWithName:#"Helvetica-Bold" size:30];
[btnStartGame setImage:
[UIImage imageNamed:#"Draw button.png"] forState:UIControlStateNormal];
btnStartGame.titleLabel.textColor=[UIColor redColor];
btnStartGame.clipsToBounds = YES;
btnStartGame.layer.cornerRadius = 50;//half of the width
btnStartGame.layer.borderColor=[UIColor redColor].CGColor;
btnStartGame.layer.borderWidth=2.0f;
btnStartGame.tag=20;
btnStartGame.highlighted=NO;
There is a really great tutorial here with downloadable source code:
http://iphonedevelopment.blogspot.co.uk/2010/03/irregularly-shaped-uibuttons.html
In essence you need to create a category on UIImage, which checks whether the point you have touched is transparent image or not. This means you can use it to check hit tests on irregular shapes.
You then subclass UIButton and over-ride the hitTest:withEvent:
Hope this helps
Although the solution by Jeff Lamarche linked by the other answer works fine, it uses a lot of memory, and/or does a lot of work:
If you create NSData once in the initializer, you end up holding a relatively large block of memory for the lifetime of each button
If you make it transient, the way it is done in the answer, then you end up converting the entire image each time you hit test your button - processing thousands of pixels and throwing away the result after fetching a single byte!
It turns out that you can do this much more efficiently, both in terms of memory use and CPU consumption, by following the approach outlined in this answer.
Subclass UIButton, and override its pointInside:withEvent: method like this:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (![super pointInside:point withEvent:event]) {
return NO;
}
unsigned char pixel[1] = { 0 };
CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 1, NULL, (CGBitmapInfo)kCGImageAlphaOnly);
UIGraphicsPushContext(context);
UIImage *image = [self backgroundImageForState:UIControlStateNormal] ;
[image drawAtPoint:CGPointMake(-point.x, -point.y)];
UIGraphicsPopContext();
CGContextRelease(context);
return pixel[0] != 0;
}
The code above takes alpha from the background image of the button. If your button uses another image, change the line that initializes image variable above.
Here I'm in a situation, where I need to add three images to make a button.
We have three parts of an image the left mast part, the middle part and the right end part.
Left part of the image -> Middle part of the image -> Right part of the image ->
Since the text in the middle i.e the title of the button varies from button to button I dont want to create a seperate image with different dimensions everytime. I think there should be a way to merge these buttons to make one complete button depending on the width of the button provided.
When all is done, the complete button should look something like this.
I hope you got what I need. Please suggest some solutions.
To do this you use a stretchable image.
The image you make will be a single image with the left part, the middle part and the right part.
The middle part only needs to be a single pixel wide. (Imagine what your button would look like with only a tiny space between the two ends. That's your image.
Then you use this code...
UIImage *image = [UIImage imageNamed:#"MyImage"];
UIImage *stretchableImage = [image stretchableImageWithLeftCapWidth:10 topCapHeight:0];
[myButton setBackgroundImage:stretchableImage forState:UIControlStateNormal];
This will then take that narrow image and take the pixel AFTER your cap width (i.e. 10 in this example) and it will repeat that single pixel the required number of times to get the full width of the button.
You can then reuse this single image everywhere no matter how small or big the buttons are.
You can use button title label to set text while keeping the background image common.
eg :- Works
[btn setTitle:#"something" forState:UIControlStateNormal];
[btn setTitle:#"something" forState:UIControlStateHighlighted];
[btn setTitle:#"something" forState:UIControlStateDisabled];
[btn setTitle:#"something" forState:UIControlStateSelected];
You can set image to button using the
setImage:forState method or setBackgrounImage:forState
eg :-
btnImage = [UIImage imageNamed:#"image.png"];
[btnTwo setImage:btnImage forState:UIControlStateNormal];
//OR setting as background image
[btnTwo setBackgroundImage:[UIImage imageNamed:#"name.png"] forState:UIControlStateNormal];
You can use different font that is used in the image by using the font property of UIButton
eg :- btn.titleLabel.font = [UIFont systemFontOfSize:size];
//OR
You can also set the font size, and the font style using something like this. Its a little more than what your asking for but hey, what the heck...
[btn.titleLabel setFont:[UIFont fontWithName:#"Helvetica-Bold" size:13.0]];
And... If you're feeling frisky a list of available fonts can be found by implementing this code and then checking the output in your xCode debugger.
Code:
NSArray *familyNames = [[NSArray alloc] initWithArray:[UIFont familyNames]];
NSArray *fontNames;
NSInteger indFamily, indFont;
for (indFamily=0; indFamily<[familyNames count]; ++indFamily)
{
NSLog(#"Family name: %#", [familyNames objectAtIndex:indFamily]);
fontNames = [[NSArray alloc] initWithArray:
[UIFont fontNamesForFamilyName:
[familyNames objectAtIndex:indFamily]]];
for (indFont=0; indFont<[fontNames count]; ++indFont)
{
NSLog(#" Font name: %#", [fontNames objectAtIndex:indFont]);
}
}