I am developing a similar program like Photos in the iPhone using ALAssetLibrary. I am trying to load the images in a scrollview. Everything works fine when the album has small amount of pictures. But when I trying to load the album with 200+ photos, my program ended without any error message. Anyone know this program?
Here is my code for loading scroll view:
- (void)loadScrollView
{
for (UIView *v in [scrollview subviews]) {
[v removeFromSuperview];
}
CGRect scrollFrame = [self frameForPagingScrollView];
scrollview = [[UIScrollView alloc] initWithFrame:scrollFrame];
CGRect workingFrame = scrollview.frame;
workingFrame.origin.y = 0;
photoCount = [info count];
for(NSDictionary *dict in info) {
UIImageView *imageview = [[UIImageView alloc] initWithImage:[dict objectForKey:UIImagePickerControllerOriginalImage]];
[imageview setContentMode:UIViewContentModeScaleAspectFit];
imageview.frame = workingFrame;
[scrollview addSubview:imageview];
[imageview release];
[scrollview setPagingEnabled:YES];
[scrollview setDelegate:self];
[scrollview setAutoresizesSubviews:YES];
[scrollview setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[scrollview setShowsVerticalScrollIndicator:NO];
[scrollview setShowsHorizontalScrollIndicator:NO];
workingFrame.origin.x = workingFrame.origin.x + workingFrame.size.width;
}
[self setScrollViewContentSize];
[[self view] addSubview:scrollview];
}
Thanks a lot in advance!!!
I personally put all of my UIImageView objects on my UIScrollView, but only set the image property for them for those that are currently visible (and clear the image property for those that are no longer visible). If you have thousands of images, perhaps even that is too wasteful (perhaps you don't even want to keep the UIImageView objects, even without their image property set, around), but if you're dealing with hundreds, I find it is a nice easy solution, addressing the key problem of the memory consumed by the UIImage objects:
- (void)viewDidLoad
{
[super viewDidLoad];
self.scrollView.delegate = self;
// all of my other viewDidLoad stuff...
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self loadVisibleImages];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self loadVisibleImages];
}
- (void)loadVisibleImages
{
CGPoint contentOffset = self.scrollView.contentOffset;
CGRect contentFrame = self.scrollView.bounds;
contentFrame.origin = contentOffset;
for (UIImageView *imageView in _imageViews) // _imageViews is (obviously) my array of images
{
if (CGRectIntersectsRect(contentFrame, imageView.frame))
{
imageView.image = ... // set the image property
}
else
{
imageView.image = nil;
}
}
}
This is a snippet from some code that's doing a bunch of other stuff, so clearly your implementation will differ significantly, but it shows how you can use scrollViewDidScroll to determine what images are visible and load/unload images appropriately. You could probably alter further if you want to also remove/add the UIImageView objects, too, but clearly the logic of "is this imageview visible" would have to be changed, rather than leveraging the frame of all of the UIImageView objects.
I'm not sure if I'm reading your code right, but do you also have all of your UIImage objects sitting in a dictionary, too? That's pretty extravagant use of memory, itself. I usually keep the actual images in some persistent store (e.g. I use Documents folder, though you could use Core Data or SQLite, though the latter two impose a significant performance hit for large images). The only images I keep in memory are the ones actively used by the UI and I'll use a NSCache object to keep a few around for performance reasons, but I otherwise pull them from persistent storage, not active memory.
You should use scroll view delegation to determine which images would be showing on the screen at the current time, and only have those loaded in memory.
Also, if you are displaying a much smaller than the actual image size, you should resize the image and use the smaller image.
why not use UICollectionView and UICollectionViewController classes? UICollectionViewController reference here
sample code here.
Your images will end up being instances of UICollectionViewCell. The data source and delegate protocol methods are similar to the UITableView methods. i.e. they provide a mechanism where the UICollectionViewController will reuse the UICollectionViewCells.
The benefit of using these classes is that they are used similarly to UITableViewControllers and they assist you with the memory pressure issues that you are having.
good luck!
Related
So I use Core Data to store a few images. (Also the subclass is generated with latest mogenerator)
(And also I'm using ARC)
Yeah I know I could just keep a reference and store it on the disk, but I thought :
"Hey they made an option so I can do just that without having to manage it myself!"
So I tried it and it works perfectly except all the data loaded that way is never released.
In the initialization of the ViewController who's gonna be in charge of displaying the images I give it the usual main NSManagedObjectContext.
And in a method called in viewDidAppear I set up the UIScrollView with the images :
Edit :
So it's not really a fetch request I have an Entity1 which have one-to-many with images and I use it to get the images
I get this entity1 from the same context. I just wanted to simplify to explain better.
- (void)setupScrollViewWithEntity1:(Entity1 *)entity1 {
DDLogVerbose(#"-- %# : SETUP SCROLL VIEW --", self);
// I remove any previous subviews
[self.scrollView.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
self.scrollView.contentSize = self.scrollView.frame.size;
/* Here I get the imagesArray with a NSFetchRequest */
//So it's not really a fetch request I have an `Entity1` which have one-to-many with images and I use it to get the images
NSSet *imagesSet = entity1.images;
// So I have an NSArray holding all the Image object
for (Image *image in imagesSet) {
CGRect frame = self.scrollView.frame;
frame.origin.x = image.numberValue*frame.size.width;
UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:frame];
scrollView.contentSize = self.scrollView.frame.size;
UIImageView *imageView = [[UIImageView alloc]initWithFrame:self.scrollView.frame];
imageView.image = [UIImage imageWithData:image.image];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.userInteractionEnabled = YES;
[scrollView addSubview:imageView];
scrollView.delegate = self;
scrollView.minimumZoomScale = 1.0;
scrollView.maximumZoomScale = 3.0;
[self.scrollView addSubview:scrollView];
}
}
}
In viewWillDisappear I save the NSManagedObjectContext and I would expect when the controller gets dealloc'ed that all the data would too, but it stays in memory forever.
This Line somehow retains it don't know why : imageView.image = [UIImage imageWithData:image.image];
I spent 3 days on it trying to use everything, Instruments, rechecked if I didn't keep a strong reference somewhere.
The thing is the UIViewController gets dealloc'ed that I'm sure of, I see it in Instruments with the Allocation tool but for some reason the data stays in memory forever until the app crashes.
Here's the list of the ~20 images in memory not getting dealloc'ed :
And here's the details for the first object :
Thank you for reading until here, I'm really desperate :(
Have you tried calling reset on the NSManagedObjectContext after you are done with the images? That evicts any loaded objects from the in-memory object graph. They will then need to be fetched from disk/db next time they are required.
i have my subviews stored in an array...what i want is to place these subviews to my main view from array....
for(int i=0;i<[imageViewArray count];i++)
{
NSLog(#" image array count is %d",[imageViewArray count]);
// countlayers= countlayers+1;
// [canvasView insertSubview:[imageViewArray objectAtIndex:i] atIndex:countlayers];
[canvasView addSubview:[imageViewArray objectAtIndex:i]];
}
can you tell me what i am doing wrong
If imageViewArray really contains initialized views, and if those views already have correct frame attributes, you're doing nothing wrong (even though your code could be more elegant).
Also, of course, we have to make sure that canvasView is indeed initialized and visible on the screen. Try setting its backgroundColor to something noticeable:
canvasView.backgroundColor = [UIColor greenColor];
Once you are sure the canvasView is actually being displayed, I would suggest using fast enumeration, which makes going through the array nicer, and logging your new subviews to make sure their frames are like you want them.
for (UIView *imageView in imageViewArray) //this is fast enumeration. You can get it through code completion by typing forin
{
NSLog(#"%#", imageView); //let's take a look at the imageView. What is its frame?
[canvasView addSubview:imageView];
}
Used enumeration to add images on your canvas View using imageViewArray and probably you are also missing with the setting frame of the images.
for (UIImageView *imageView in imageViewArray) //enumeration.
{
//let's set frame for image view before adding to canvasView
[imageView setFrame:CGRectMake(x,y,w,h)];
//Also make sure imageView has image or not
if(imageView.image)
[canvasView addSubview:imageView];
}
Basically, I want to have an app with only one view that has an image on it. I want to be able to swipe left or right and have the first image go out of the view and the second image to come in. The images are the same and I want it to look like they are connected (like scrolling down a rope where the pattern just repeats, but it looks like a constant scroll). I need it to be able to change the image or restart after a series of swipes. I know that I need to turn pagination ON in the UIScrollView, but I am new to iOS and am having trouble.
Ultimately, I want to have the iPhone vibrate every so-and-so swipes (and restart the pattern).
I'm sure that there are a lot of ways to do this (i.e. a TableView) so feel free to just point me in the direction of some references if the answer is tedious to explain.
Thanks!
FOLLOW UP:
I found an Apple example that did very nearly what I wanted to do. I made a lot of adjustments to it, but I'm banging my head against a wall trying to get the images to cycle. Here is what I think is the offending code, but I'm not sure what the solution is, as the ScrollView is functional, it just doesn't reset the center to the current view. Any ideas?
- (void)layoutScrollImages
{
UIImageView *view = nil;
NSArray *subviews = [scrollView1 subviews];
// reposition all image subviews in a horizontal serial fashion
CGFloat curXLoc = 0;
for (view in subviews)
{
if ([view isKindOfClass:[UIImageView class]] && view.tag > 0)
{
CGRect frame = view.frame;
frame.origin = CGPointMake(curXLoc, 0);
view.frame = frame;
curXLoc += (kScrollObjWidth);
}
}
// set the content size so it can be scrollable
[scrollView1 setContentSize:CGSizeMake((kNumImages * kScrollObjWidth), [scrollView1 bounds].size.height)];
}
I'd just use a UIScrollView. Set the contentWidth to be 3 times the width/height of the view (for 3 pages) and set the contentOffset to be the center 'page' (view.bounds.size.width or view.bounds.size.height depending on whether you're scrolling horizontally/vertically respectively) . You'll need to setup a delegate for the UIScrollView (probably the view controller) and implement - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView. This will be called when the scroll view has finished decelerating. Once it has finished decelerating, reset the contentOffset back to the center view. This should give the impression of an infinite scroll. You can also set a counter to increment in the scrollViewDidEndDecelerating method to increment the counter or initiate the vibration.
You shouldn't need to keep repositioning the images. Just set the images once in the scrollView:
//Horizontal arrangement
UIImage *image = [UIImage imageNamed:#"nameOfImage.png"];
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:image];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:image];
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:image];
NSArray *imageViews = [NSArray arrayWithObjects:imageView1, imageView2, imageView3];
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview: scrollView]; //This code assumes it's in a UIViewController
CGRect cRect = scrollView.bounds;
UIImageView *cView;
for (int i = 0; i < imageViews.count; i++){
cView = [imageViews objectAtIndex:i];
cView.frame = cRect;
[scrollView addSubview:cView];
cRect.origin.x += cRect.size.width;
}
scrollView.contentSize = CGSizeMake(cRect.origin.x, scrollView.bounds.size.height);
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0); //should be the center page in a 3 page setup
So the images are setup, you don't need to mess with them anymore. Just reset the contentOffset when the scroll views stops (note: you need to make sure you're the delegate of the scroll view or you'll not receive the message when the scroll view stops):
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
}
Please forgive any typos. I wrote it out by hand.
Look on cocoacontrols.com for a custom photo album view. As for the vibration, this code snippet vibrates the phone (make sure you link to and #import <AudioToolbox/AudioToolbox.h>):
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
I have a UIScrollViewController that has a UIScrollView which holds an UIImageView inside.
In loadView of the the scroll view controller I download the image by calling a function, that's supposed to do the downloading on a different thread. I call this function from my loadView and then put the imageview inside the scrollview, and set the scrollview as the view of the controller.
The problem is I can't see the image when I run the program, after clicking a row in the tableView (which is supposed to push the scrollview with the image in it). However, if I change tabss (in the tabbarviewcontroller) and come back to this tab. The image will show.
So I think the image download happens, but I somehow have a problem showing it instantly on the screen. It only appears after I come back to it. What do I seem to be doing wrong? I'm new to threads so I suspect it's a problem with that. Also my code was working before I made it so that it would do the download in another thread, so I'm pretty sure it is related to that.
This is the function in the Photo.m which is an Entity in Core Data. This is supposed to do the download
- (void)processImageDataWithBlock:(void (^)(NSData *imageData))processImage {
NSString *url = self.imageURL;
dispatch_queue_t callerQueue = dispatch_get_current_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("Flickr download", NULL);
dispatch_async(downloadQueue, ^{
NSData *imageData = [FlickrFetcher imageDataForPhotoWithURLString:url];
dispatch_async(callerQueue, ^{
processImage(imageData);
});
});
}
This is my loadView method in the PhotoScrollViewController.m
- (void)loadView {
[image processImageDataWithBlock:^(NSData *imageData) {
UIImage *imageToBeShown = [UIImage imageWithData:imageData];
imageView = [[UIImageView alloc] initWithImage:imageToBeShown];
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
scrollView = [[UIScrollView alloc] initWithFrame:applicationFrame];
scrollView.delegate = self;
scrollView.contentSize = imageToBeShown.size;
[scrollView addSubview:imageView];
self.title = image.title;
self.view = scrollView;
}];
}
Edited to add extra information that was added as an answer
Adding [super loadView] to my loadView method solved the issue. However, the documentation says that I shouldn't be calling super loadView from my loadView.
I tried moving the code to viewDidLoad and that works as well. But really this is the code that's setting the view so I feel like I should be putting it in loadView. But then it doesn't work when I use this multi threading mechanism for download.
Is this because the download is somehow interfering with me setting the view in loadView?
It seems loadView completes before the image download is done. You need some way of calling [imageView setNeedsDisplay] when image download is complete. You need some method that gets called when image download is done that accesses the imageView (perhaps using viewWithTag) from the scrollView and calls [imageView setNeedsDisplay];
I have a large UIScrollView into which I'm placing 3-4 rather large (320x1500 pixels or so) UIImageView image tiles. I'm adding these UIImageViews to the scroll view inside of my NIB files. I have one outlet on my controller, and that is to the UIScrollView. I'm using a property (nonatomic, retain) for this, and sythesizing it.
My question is this: When I observe this in Memory Monitor, I can see that the memory used goes up quite a bit when the view with all these images is loaded (as expected). But when I leave the view, it and its controller are dealloc'd, but do not seem to give up anywhere near the memory they had taken up. When I cut one of these views (there are several in my app) down to just 1-3 images that were 320x460 and left everything else the same, it recaptures the memory just fine.
Is there some issue with using images this large? Am I doing something wrong in this code (pasted below)?
This is a snippet from the viewController that is causing problems.
- (CGFloat)findHeight
{
UIImageView *imageView = nil;
NSArray *subviews = [self.scrollView subviews];
CGFloat maxYLoc = 0;
for (imageView in subviews)
{
if ([imageView isKindOfClass:[UIImageView class]])
{
CGRect frame = imageView.frame;
if ((frame.origin.y + frame.size.height) > maxYLoc) {
maxYLoc = frame.origin.y;
maxYLoc += frame.size.height;
}
}
}
return maxYLoc;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.scrollView setContentSize:CGSizeMake(320, [self findHeight])];
[self.scrollView setCanCancelContentTouches:NO];
self.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
self.scrollView.clipsToBounds = YES;
self.scrollView.scrollEnabled = YES;
self.scrollView.pagingEnabled = NO;
}
- (void)dealloc {
NSLog(#"DAY Controller Dealloc'd");
self.scrollView = nil;
[super dealloc];
}
UPDATE: I've noticed another weird phenomenon. If I don't use the scroll on the view, it seems to be hanging on to the memory. But if I scroll around a bunch and ensure that all of the UIImageViews became visible at one point, it will free up and regain most of the memory it lost.
UPDATE2: The reason I'm asking this is my app is actually crashing due to low memory. I wouldn't mind if it were just caching and using up extra memory, but it doesn't seem to ever release it - even in didReceiveMmoryWarning conditions
I've solved the mystery - and I'm pretty sure this is a bug on Apple's side.
As Kendall suggested (thanks!), the problem lies in how InterfaceBuilder loads images from the NIB file. When you initFromNib, all UIImageViews will init with a UIImage using the imageNamed: method of UIImage. This call uses caching for the image. Normally, this is what you want. However, with very large images and additionally ones that scroll far off of the visible area, it does not seem to be obeying memory warnings and dumping this cache. This is what I believe to be a bug on Apple's side (please comment if you agree/disagree - I'd like to submit this if others agree). As I said above, the memory used by these images does seem to be released if a user scrolls around enough to make it all visible.
The workaround that I've found (also Kendall's suggestion) is to leave the image name blank in the NIB file. So you lay out your UIImageView elements as normal, but don't select an image. Then in your viewDidLoad code, you go in and load an image using imageWithContentsOfFile: instead. This method does NOT cache the image, and therefore does not cause any memory issues with retaining large images.
Of course, imageNamed: is a lot easier to use, because it defaults to anything in the bundle, rather than having to find the path. However, you can get the path to the bundle with the following:
NSString *fullpath = [[[NSBundle mainBundle] bundlePath];
Putting that all together, here's what that looks like in code:
NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:#"/%#-%d.png", self.nibName, imageView.tag]];
UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
imageView.image = loadImage;
So adding that to my code above, the full function looks like this:
- (CGFloat)findHeight
{
UIImageView *imageView = nil;
NSArray *subviews = [self.scrollView subviews];
CGFloat maxYLoc = 0;
for (imageView in subviews)
{
if ([imageView isKindOfClass:[UIImageView class]])
{
CGRect frame = imageView.frame;
if ((frame.origin.y + frame.size.height) > maxYLoc) {
maxYLoc = frame.origin.y;
maxYLoc += frame.size.height;
}
NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:#"/%#-%d.png", self.nibName, imageView.tag]];
NSLog(fullpath);
UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
imageView.image = loadImage;
}
}
return maxYLoc;
}
Separate from your imageNamed caching issues, the answer to your question
I have a large UIScrollView into which
I'm placing 3-4 rather large (320x1500
pixels or so) UIImageView image tiles.
[...] Is there some issue with using
images this large?
Is in the header of the UIImage docs:
You should avoid creating UIImage objects that are greater than 1024 x 1024 in size.
Besides the large amount of memory such an image would consume, you may run into
problems when using the image as a texture in OpenGL ES or when drawing the image
to a view or layer.
Also, Apple claims to have fixed the imageNamed cache-flushing problem in 3.0 and beyond, although I've not tested this extensively, myself.
It could be the system caching references to your images in memory, assuming you have just drug in the images from the media browser in IB the code underneath is probably using the UIImage imageNamed: method...
You could try loading all the images with imageWithContentsOfFile: , or imageWithData: and see if it behaves the same (dragging in unfilled UIImageViews in IB and setting contents in viewDidLoad:).
Read the UIImage class reference if you'd like a little more detail, it also describes which methods are cached.
If it's the cache it's probably OK though as the system would free it if needed (did you also try hitting the Simulate Memory Warning in the simulator?)
Completing the answer of Bdebeez.
One nice idea is to override the imageNamed: calling the imageWithContentsOfFile:.
Here is the idea of the code:
#implementation UIImage(imageNamed_Hack)
+ (UIImage *)imageNamed:(NSString *)name {
return [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/%#", [[NSBundle mainBundle] bundlePath], name ] ];
}
#end
Note: With this override you will not have any cache loading UIImages, if you need this, you will have to implement your own cache.
- (void)dealloc {
NSLog(#"DAY Controller Dealloc'd");
[self.scrollView release];
[super dealloc];
}
give that a shot, your #property() definition is requesting it to be retained, but you weren't explicitly releasing the object
From what I learned on its memory management behavior is that views won't get dealloc unless low in memory. Try an official demo like SQLBooks: 1. Run with Leaks Monitor 2. Run through every views it has. 3. Go back to the root view. 4. You will notice the memory usage level is still the same. As Kendall said it may be cached?
I think you shouldn't pay attention on this memory usage pattern -- because when the new images are pointed to the UIScrollView, the old image objects will be released and memory will be freed for new images anyway.
There is a known problem - a memory leak in imageName. I found a really useful solution for it - creating image cash in application delegate, this way optimizing the performance and memory usage in my application.
See this blog post