I want to display multiple images in scrollview when images are more than 70 application will crash and display received memory error.I have get the images from document Directory
I have tried -
UIScrollView *MyScroll=[[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 480)];
int x=5,y=15;
for (int i = 0; i < imgarr.count; i++)
{
if(x<=211)
{
UIImage* imagePath = [UIImage imageWithContentsOfFile:[path_array objectAtIndex:i]];
imgView=[[UIImageView alloc]initWithFrame:CGRectMake(x, y, 77, 75)];
imgView.image=imagePath;
imgeBtn=[UIButton buttonWithType:UIButtonTypeCustom];
imgeBtn.frame=CGRectMake(x, y, 93, 110);
[imgeBtn addTarget:self action:#selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];
imgeBtn.tag=i;
x+=103;
}
else
{
x=5;
y+=130;
UIImage* imagePath = [UIImage imageWithContentsOfFile:[path_array objectAtIndex:i]];
imgView=[[UIImageView alloc]initWithFrame:CGRectMake(x, y, 77, 75)];
imgView.image=imagePath;
imgeBtn=[UIButton buttonWithType:UIButtonTypeCustom];
imgeBtn.frame=CGRectMake(x, y, 93, 110);
[imgeBtn addTarget:self action:#selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];
imgeBtn.tag=i;
x+=103;
}
[MyScroll addSubview:imgeBtn];
[MyScroll addSubview:imgView];
MyScroll.contentSize=CGSizeMake(320, y+120);
How can I display multiple images in scrollview?
The advice others have given to switch to a UICollectionView is good, but it has missed the real flaw in your code: the method imageNamed: of UIImage is keeping all 70 of your images in memory forever. Per the docs:
If you have an image file that will only be displayed once and wish to
ensure that it does not get added to the system’s cache, you should
instead create your image using imageWithContentsOfFile:. This will
keep your single-use image out of the system image cache, potentially
improving the memory use characteristics of your app.
Switching to this method will prevent the crashes, if you only load into memory the images that you need. UICollectionView makes this trivial, since you can only load image when the collection view asks you to fill out a cell object in its datasource's collectionView:cellForItemAtIndexpath: method.
Create custom class of collection view cell
and You can pass image name in array.
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController<UICollectionViewDataSource,UICollectionViewDelegate>
#property (weak, nonatomic) IBOutlet UICollectionView *Collection;
#end
#import "ViewController.h"
#import "CustomCell.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.Collection registerClass:[CustomCell class] forCellWithReuseIdentifier:#"cell"];
[self.Collection registerNib:[UINib nibWithNibName:#"CustomCell" bundle:nil] forCellWithReuseIdentifier:#"cell"];
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 5;
}
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 4;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell * cell=[collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
cell.imgview.image=[UIImage imageNamed:#"XYZ.png"];
return cell;
}
You are allocating and keep adding the images which causing the memory crash as all the images taking the device memory, There are many solution which build for showing the images and best is using the UICollectionView which load only the images which is showing the screen. You go for any good tutorial which can show you how to do it, here is the reference
Maybe NSCache can help you.
1
When you are displaying image at index 10.You can cache image 5-9 and 11-16 to NSCache/
2
When scrollView start Scrolling, get image from NSCache you need,and remove the object you doesn't need to display.
3
I think this can be efficient. Apple provide a demo how they handle the massive assets using caches and UICollectionView.You take a look at it.
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
I have also faced this same issue which i have resolved using below simple line.
instead of using this:
[UIImage imagedNamed:#"yourImage"];
try using this:
[yourCell.imageview setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:#"yourImage" ofType:#"png"]]];
i hope it would solve your problem as by using imageWithContentsOfFile releases memory. so you won't get Memory Warning
UPDATE
You may also display images from any Remote URL and also from your application's document directory using other methods of UIImage Class
do tell if it helps
You may try with UITableView . Suppose you have n number of Images then -
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// 3 is numbers of images per row
if (n/3 *3 == n)
return n/3;
return n/3 +1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
}
int x=5,y=15;
UIImageView* imgView1=[[UIImageView alloc]initWithFrame:CGRectMake(x, y, 93, 110)];
imgView1.image=[UIImage imageNamed:[imgarr objectAtIndex:indexPath.row + 0]];
x+=103;
UIImageView* imgView2=[[UIImageView alloc]initWithFrame:CGRectMake(x, y, 93, 110)];
imgView2.image=[UIImage imageNamed:[imgarr objectAtIndex:indexPath.row + 1]];
x+=103;
UIImageView* imgView2=[[UIImageView alloc]initWithFrame:CGRectMake(x, y, 93, 110)];
imgView2.image=[UIImage imageNamed:[imgarr objectAtIndex:indexPath.row + 2]];
[cell.contentView addSubview:imgView1];
[cell.contentView addSubview:imgView2];
[cell.contentView addSubview:imgView3];
return cell;
}
Use #autoreleasepool. Here are details about #autoreleaspool
The #autoreleasepool statement is doing the same job as before, instead of using the NSAutoreleasePool class. The way the NSAutoreleasePool worked was a bit weird, as creating it caused an effect throughout the whole application; #autoreleasepool creates a scoped area and makes it clearer what's within the pool and when it drains (when it goes out of scope). It's also more efficient according to Apple.
The concept of an autorelease pool is simple, whenever an object instance is marked as autoreleased (for example NSString* str = [[[NSString alloc] initWithString:#"hello"] autorelease];), it will have a retain count of +1 at that moment in time, but at the end of the run loop, the pool is drained, and any object marked autorelease then has its retain count decremented. It's a way of keeping an object around while you prepare whatever will retain it for itself.
With ARC, whilst the autorelease keyword isn't used by the developer, the underlying system that manages ARC inserts that for you. (Remember: All ARC is doing is inserting retain, release and autorelease calls for you at the appropriate times). Because of this, the existing AutoreleasePool concept needs to stay around.
If you remove the autorelease pool, your objects will start leaking
For more details check here
Related
I am creating an app to display listings and each listing has an amount of x images to be displayed in a table cell.
To display the images, I have to create the UIImageView dynamically and load the images into the cell via a for loop (depending on the data received from a server call).
Now, I am able to add the images dynamically, but when I scroll the table view, the cellForRowAtIndexPath function runs again and the images are loaded again into the cell, hence creating more image views than the actual data.
I want to keep the image count constant in the cell and do not want to create more images in the cell as the table scrolls.
Here is the function code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *Cellidentifier1 = #"ClassCell";
ClassCell *cell = [tableView dequeueReusableCellWithIdentifier:Cellidentifier1 forIndexPath:indexPath];
// Configure the cell...
long row = [indexPath row];
for (int t = 0; t<individualSports.count; t++) {
UIImageView * imageView = [[UIImageView alloc]initWithFrame:CGRectMake((250/10*count+10), 125, 20, 20)];
[imageView setImage:[UIImage imageNamed:#"cricket_unselected.png"]];
[cell addSubview:imageView];
}
return cell;
}
Your ClassCell could take care of this for you. If it is used for other purposes, just subclass it again;
#implementation ClassCell // or a new ClassCell subclass
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// for loop..
}
return self;
}
#end
Alternatively, you could just use a BOOL:
#interface ClassCell
#property BOOL hasImageViews;
#end
and then:
if (!cell.hasImageViews) {
cell.hasImageViews = YES;
// for loop..
}
Side note: I'm not really sure why you want to add the same picture that many times to a single cell, though; isn't it more likely that you want to use a checkbox of some kind? Also, you're iterating with t, but then applying the frame with count, meaning that all of your image views are overlapped on each other because they have the same frame within the cell.
I have multiple collection views on screen at one time that scroll horizontally. They are all filled with images. All of these images are being loaded in the background through the Parse api. I am running Instrument's allocations and the anonymous VM: ImageIO_JPEG_DATA category is taking up a majority of the memory being used. All memory in the app equals to about 40 and then this category is over 55, which puts the total right around 100. That category never goes down at all and just stays consistent. What can I do to free up this memory from the images in my collection views?
Here is the code for my collection view:
.m for my collection view controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionViewCell" forIndexPath:indexPath];
if (cell) {
[cell.loadingImageIndicator setHidden:NO];
[cell.loadingImageIndicator startAnimating];
id photo = [self.collectionViewPhotos objectAtIndex:indexPath.item];
if ([photo isKindOfClass:[PFObject class]]) {
PFObject *photoObject = (PFObject *)photo;
PFFile *imageFile = [photoObject objectForKey:kPhotoPictureKey];
[cell.cellImage setFile:imageFile];
[cell.cellImage loadInBackground:^(UIImage *image, NSError *error) {
[cell.cellImage setContentMode:UIViewContentModeScaleAspectFill];
[cell.loadingImageIndicator stopAnimating];
[cell.loadingImageIndicator setHidden:YES];
}];
} else if ([photo isKindOfClass:[UIImage class]]) {
[cell.cellImage setImage:photo];
[cell.cellImage setContentMode:UIViewContentModeScaleAspectFill];
}
}
return cell;
}
.m for CollectionViewCell
- (void)prepareForReuse
{
self.cellImage.image = nil;
}
- (void)dealloc
{
self.cellImage = nil;
}
Edit: Photo of instruments
I had the same issue in a photo gallery-type app, and ran into the same problem with allocations in the so-called ImageIO_JPEG_DATA category accumulating and remaining "live" forever, causing my app to run out of memory. Oddly, this happenned only on the iPad I was testing on and NOT on the ios simulator (which showed no memory problems).
Brian's suggestion (below) worked for me. My app was originally using an array, each element of which contained - amongst other things - a UIImage. The images were used in various UIScrollViewControllers.
When I needed to load an image, if I used
[UIImage imageWithContentsOfFile:path]
rather than a direct reference to the UIImage in my array, the memory problem (caused by some inexplicable caching that ImageIO_Malloc was doing) was gone and the ImageIO_JPEG_DATA allocations stopped piling up and got released.
I would never have come up with this solution in a gazillion years on my own, so thanks to Brian.
I am stucked in a stupid problem since two days. I have got a UITableViewController pushed in Navigation Controller. When it loads, since there is no data, so empty table is visible:
But when I receive data from server, and call [self.tableView reloadData], both numberOfRowsInSection and heightForRowAtIndexPath get invoke except cellForRowAtIndexPath and my controller is shown without table:
I can't really understand that why it is happening. All datasource methods are called except for cellForRowAtIndexPath. Please someone guide me... Thanks..
ActLogController.h
#interface ActLogController : UITableViewController<ASIHTTPRequestDelegate,UITableViewDataSource,UITableViewDelegate>
#property(strong) NSMutableArray *activityKeys;
#end
ActLogController.m
- (void)viewDidLoad
{
[super viewDidLoad];
activityKeys = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self retrieveActivityLogFromServer];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return activityKeys.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
ActivityLogUnit *act = [activityKeys objectAtIndex:indexPath.row];
cell.textLabel.text = act.price;
return cell;
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
-(void) requestFinished:(ASIHTTPRequest *)request
{
NSArray *list = [request.responseString JSONValue];
for (int i = 0; i < list.count; i++) {
NSArray *singleTrade = [[list objectAtIndex:i] JSONValue];
ActivityLogUnit *unit = [[ActivityLogUnit alloc] init];
unit.symbol = [singleTrade objectAtIndex:0];
unit.type = [singleTrade objectAtIndex:1];
unit.price = [singleTrade objectAtIndex:2];
unit.volTraded = [singleTrade objectAtIndex:3];
unit.remVol = [singleTrade objectAtIndex:4];
unit.actualVol = [singleTrade objectAtIndex:5];
unit.recordID = [singleTrade objectAtIndex:6];
unit.orderNo = [singleTrade objectAtIndex:7];
unit.time = [singleTrade objectAtIndex:8];
[activityKeys addObject:unit];
}
if(activityKeys.count > 0)
{
[self.tableView reloadData];//it is called and I have got 6 items confirm
}
}
EDIT
I set some dummy data in my array activityKeys, Data is being displayed in table, and cellforrowatindexpath is called successfully. But as I reload data after sometime, other methods are called except this one and table disappears as shown in 2nd pic. Any ideas?
Your problem is that you probably download the data content on a background thread. Since you cannot update the UI on a background you need to call [self.tableView reloadData] on the main thread once the download is finished!
Hope it helps!
Looks like you in secondary thread, do reloadData in main thread by using following code
[self.tableView performSelectorOnMainThread#selector(reloadData) withObject:nil waitUntilDone:NO]
You can always use [NSThread isMainThread] to check whether you are in main thread or not.
you have to write in viewdidload
self.tableView.dataSource = self;
self.tableView.delegate = self;
Edit
You have no xib then where you are declared/sets your tableview's properties. Like
self.tableView.frame = CGRectMake(0, 45, 320, 500);
self.tableView.rowHeight = 34.0f;
self.tableView.separatorStyle=UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.tableView];
[self.tableView setBackgroundColor:[UIColor clearColor]];
self.tableView.showsVerticalScrollIndicator=NO;
self.tableView.dataSource = self;
self.tableView.delegate = self;
Try with
#property(nonatomic,strong) NSMutableArray *activityKeys;
Firstly I strongly believe that the instance name of the tableview should not be similar to the local variable (i.e. tableView in class should not be equal to tableView in delegate and data source methods).
Second in your question posted I could not see the delegate set for the table view.
answer Posted By Samir Rathod should work if you have #property for the table view set in you .h or .m file.
You can also do this if you have a XIB file.
Press ctrl and click + drag the tableview to the files owner and set the delegate and datasource.
For me the problem was my stubbed-out code returning 0 as the number of sections (so it never asked how many rows were in the section, and never got their data). Just change that to 1 if it's your problem also. Additionally, I was working in Swift, where the issue mentioned by #shahid-rasheed is coded (slightly) differently:
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
At last I got it worked. cellForRowAtIndexPath was not being called because of a line of code I didn't mention here... which was actually removing some color background layer from view. It was causing reloading issue. After removing it, everything works fine.
Thank you all of you for your cooperation :)
I had the same symptoms too. In my case, the first time I loaded the data (from core data) in viewDidLoad, NSSortDescriptor was used to sort the data.
On the click of a button, the core data was fetched again (this time with changes) and tableView data reloaded. It initially gave me a blank table view after the button was clicked because I forgot to sort the data the second time I fetched it.
Learning points: Remember to call all methods which modify the cell (like background color mentioned by iAnum, or NSSortDescriptor in my case) if you have used them in the beginning!
I have a UICollectionView, which receives data from a webservice. Based on the data it receives, it draws cells on my UICollectionView. Each cell is a custom Class which extends UICollectionViewCell. Each UICollectionViewCell is loaded via a .nib. My init method looks like this :
#implementation GridCell
- (id)initWithFrame:(CGRect)frame
{
if (self)
{
// Initialization code
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:#"GridCell" owner:self options:nil];
if ([arrayOfViews count] < 1) {
return nil;
}
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
return nil;
}
self = [arrayOfViews objectAtIndex:0];
// It is important that you set the properties of the view after the above assignment
// because self is assigned to the nib after that call.
self.layer.masksToBounds = NO;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(0.0, 1.0);
self.layer.shadowOpacity = 0.17;
self.layer.shadowRadius = 0.35f;
self.layer.shouldRasterize = YES;
}
return self;
}
I have a GridViewController which is basically a UIViewController :
#implementation GridViewController
- (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight {
self = [super init];
if(self) {
// Have the getGridView returning the initialized view
// then assign it to the GridViewControllers view property
self.view = [self getGridView:frameWidth:frameHeight];
}
return self;
}
-(UIView *)getGridView:(CGFloat)width :(CGFloat)height {
UIView *gridHolder = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
UICollectionViewFlowLayout *flow = [[UICollectionViewFlowLayout alloc]init];
[flow setScrollDirection:UICollectionViewScrollDirectionHorizontal];
CGRect screen = [[UIScreen mainScreen] bounds];
CGFloat screenheight = screen.size.height;
// It is an iphone 5, setup the cellsize and the spacing between cells.
if(screenheight > 480) {
[flow setItemSize:CGSizeMake( (gridHolder.frame.size.width / 1.85) , (gridHolder.frame.size.height / 3.25) )];
[flow setSectionInset:UIEdgeInsetsMake(0, 0, 0, 0)];
[flow setMinimumInteritemSpacing:0];
[flow setMinimumLineSpacing:10];
// It is an iphone 4, setup the cellsize and the spacing between cells.
} else {
[flow setItemSize:CGSizeMake( (gridHolder.frame.size.width / 2.5) , (gridHolder.frame.size.height / 3.1) - 10)];
[flow setSectionInset:UIEdgeInsetsMake(0, 0, 0, 0)];
[flow setMinimumInteritemSpacing:0];
[flow setMinimumLineSpacing:10];
}
self.grid = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, gridHolder.frame.size.width, gridHolder.frame.size.height) collectionViewLayout:flow];
[_grid setBackgroundColor:[UIColor colorWithRed:0.961 green:0.961 blue:0.961 alpha:1]];
[_grid setDataSource:self];
[_grid setDelegate:self];
[_grid setBounces:NO];
[_grid registerClass:[GridCell class] forCellWithReuseIdentifier:Cell];
[gridHolder addSubview:self.grid];
return gridHolder;
}
This UIViewController is also my UICollectionViewDataSource. So i have the following methods for it :
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return [releases count];
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(10, 10, 10, 10);
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
GridCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:Cell forIndexPath:indexPath];
return cell; // <-- finally return the cell
}
I added an UIButton over the cell. And i connected an IBAction to the button. But each time i click on this button, i get a nullpointer exception because the GridIcon is already thrown away by ARC. Must i retain the GridIcon somewhere? Or what is the best practice to overcome this (simple?) problem?
The method which resides in my GridIcon.m is :
- (IBAction)cellClicked {
NSLog(#"test");
}
And my GridIcon.h file has an IBAction described :
#interface GridCell : UICollectionViewCell
- (IBAction)cellClicked;
#end
Edit :
jackslash, thank you for your time and effort to make me realize this really mad structure. Let me explain how i setup this project.
I have a MainViewController which i set as the RootViewController in my AppDelegate.m. Then in my MainViewController i init 2 UIVIewController's. One of them is my GridViewController which sets the UICollectionView.
So the datasource for this UICollectionView is the UIViewController i init'ed. I needed to draw a region of the screen for this UIViewController so i give the height of the other UIViewController to the GridViewController. And the getGridView method gets the height of the screen minus the height of the view of the other UIViewConroller's view.
The structure is like this :
#interface MainViewController: UIViewController
// These next two classes both extend UIViewController
#property(nonatomic, strong) GridViewController *gridViewcontroller;
#property(nonatomic, strong) BottomBarController *bottomBarcontroller;
#end
In my MainViewController.m [viewDidLoad] i both init those UIViewcontrollers and add their views as a subView to the MainViewController.
What would be the best way to accomplish this then? Should i make a UICollectionViewController separate from my GridViewController? But how does this UICollectionView knows which region of the screen to draw on?
And for the init method within my GridCell, i actually use this to draw a shadow underneath my GridCell. How would i achieve the drawing underneath my cells without this init method?
Edit 2:
In addition to my above edit:
I have an UIImageView and a UILabel on each GridCell. How would i target those in my code?
So whats happening here is messed up in quite a few ways.
Your GridCell init method is mental
You have left some objective-c method parameters unnamed (bad form)
You have a method - (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight which one would expect to take a CGSize from the name yet it takes two floats
For some reason you are using the -(id)initWithSize:: method to init your view controller
Your GridCell class files have a different name from the class they contain (bad form)
You seem to fundamentally have misunderstood how all this is supposed to work. I'm going to help you here, but bear in mind that you should take the time to understand everything here and to also go and read a lot more about iOS. Perhaps look at the iOS course on iTunes U (CS193p) which is free and will help you write better code.
1.
This is madness. If you want to load your UICollectionViewCell subclass from a xib you dont do this, you register the nib with the collection view in a UICollectionViewController subclass like this:
UINib * cellNib = [UINib nibWithNibName:#"GridCell" bundle:[NSBundle mainBundle]];
[self.collectionView registerNib:cellNib forCellWithReuseIdentifier:cellReuseIdentifier];
Then the cell is loaded from the xib file for you when you get the cell in
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
Make sure you have the class of the cell in your GridCell.xib file set to your GridCell subclass. Never write an init method like this again.
2.
-(UIView *)getGridView:(CGFloat)width :(CGFloat)height
Always name all the parameters in an Objective-c method signature.
-(UIView *)getGridViewWithWidth:(CGFloat)width andHeight:(CGFloat)height
Is miles and miles better. But you would probably better using a CGSize...
3
- (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight
In Objective-c we would expect this to take a CGSize parameter because your method is named "initWithSize". A CGSize is a C struct with a width and height. You can make one with CGSizeMake(<width>,<height>). You would make your init method look like this:
- (id)initWithSize:(CGSize)size
But you don't really want to be initing a UIViewController with a CGSize...
4
There is a reason UIViewController does not have ANY kind of init method that takes any kind of size or dimension. The idea of a view controller is that it has the size of its view set by its parent (or window) and then is responsible for drawing its contents within its view, whatever size that may be. As a UIViewController subclass In UIKit there is a place for loading other views and view controllers that are needed to display whatever it is you are supposed to display. Check out the method -(void)viewDidLoad and override it. But even so you would be much better off if you simply subclassed UICollectionViewController as you wouldn't need any sort of container and your view controller's view property would already be a collectionView along with many other benefits.
5
Rename your GridIcon.h and GridIcon.m files to be the name of the class they contain. Always follow this rule for the sanity of future you, if not everyone else who works on your project.
Finally the reason its crashing isn't because of ARC or other technology failure its because the implementation here has real fundamental issues.
UICollectionView: I'm doing it wrong. I just don't know how.
My Setup
I'm running this on an iPhone 4S with iOS 6.0.1.
My Goal
I have a table view in which one section is devoted to images:
When the user taps the "Add Image..." cell, they are prompted to either choose an image from their photo library or take a new one with the camera. That part of the app seems to be working fine.
When the user first adds an image, the "No Images" label will be removed from the second table cell, and a UICollectionView, created programmatically, is added in its place. That part also seems to be working fine.
The collection view is supposed to display the images they have added, and it's here where I'm running into trouble. (I know that I'm going to have to jump through some hoops to get the table view cell to enlarge itself as the number of images grows. I'm not that far yet.)
When I attempt to insert an item into the collection view, it throws an exception. More on that later.
My Code
I've got the UITableViewController in charge of the table view also acting as the collection view's delegate and datasource. Here is the relevant code (I have omitted the bits of the controller that I consider unrelated to this problem, including lines in methods like -viewDidLoad. I've also omitted most of the image acquisition code since I don't think it's relevant):
#define ATImageThumbnailMaxDimension 100
#interface ATAddEditActivityViewController ()
{
UICollectionView* imageDisplayView;
NSMutableArray* imageViews;
}
#property (weak, nonatomic) IBOutlet UITableViewCell *imageDisplayCell;
#property (weak, nonatomic) IBOutlet UILabel *noImagesLabel;
#end
#implementation ATAddEditActivityViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
imageDisplayView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 290, 120) collectionViewLayout:flowLayout]; // The frame rect still needs tweaking
imageDisplayView.delegate = self;
imageDisplayView.dataSource = self;
imageDisplayView.backgroundColor = [UIColor yellowColor]; // Just so I can see the actual extent of the view
imageDisplayView.opaque = YES;
[imageDisplayView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"Cell"];
imageViews = [NSMutableArray array];
}
#pragma mark - UIImagePickerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
/* ...code defining imageToSave omitted... */
[self addImage:imageToSave toCollectionView:imageDisplayView];
[self dismissModalViewControllerAnimated:YES];
}
#pragma mark - UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
#pragma mark - UICollectionViewDatasource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
[[cell contentView] addSubview:imageViews[indexPath.row]];
return cell;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [imageViews count];
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return ((UIImageView*)imageViews[indexPath.item]).bounds.size;
}
#pragma mark - Image Handling
- (void)addImage:(UIImage*)image toCollectionView:(UICollectionView*)cv
{
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
}
UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
/* ...code that sets the bounds of the image view omitted... */
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
}
#end
To summarize:
The collection view is instantiated and configured in the -viewDidLoad method
The UIImagePickerDelegate method that receives the chosen image calls -addImage:toCollectionView
...which creates a new image view to hold the image and adds it to the datasource array and collection view. This is the line that produces the exception.
The UICollectionView datasource methods rely on the imageViews array.
The Exception
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
If I'm parsing this right, what this is telling me is that the (brand new!) collection view thinks it was created with a single item. So, I added a log to -addImage:toCollectionView to test this theory:
NSLog(#"%d", [cv numberOfItemsInSection:0]);
With that line in there, the exception never gets thrown! The call to -numberOfItemsInSection: must force the collection view to consult its datasource and realize that it has no items. Or something. I'm conjecturing here. But, well, whatever: the collection view still doesn't display any items at this point, so I'm still doing something wrong and I don't know what.
In Conclusion
I get an odd exception when I attempt to add an item to a newly-minted-and-inserted collection view...except when I call -numberOfItemsInSection: before attempting insertion.
Even if I manage to get past the exception with a shady workaround, the items still do not show up in the collection view.
Sorry for the novel of a question. Any ideas?
Unfortunately the accepted answer is incorrect (although it's on the right track); the problem is that you were calling reloadData & insertItems when you should have just been inserting the item. So instead of:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
Just do:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
Not only will this give you a nice animation, it prevents you from using the tableview inefficiently (not a big deal in a 1-cell collection view, but a huge problem for larger data sets), and avoids crashes like the one you were seeing, where two methods were both trying to modify the collection view (and one of them -- reloadData -- does not play well with others).
As an aside, reloadData is not very UICollectionView-friendly; if you do have a sizable &/or complex collection, and an insertion happens shortly before or after a call to reloadData, the insertion might finish before the reloadData finishes -- which will reliably cause an "invalid number of items" crash (same goes for deletions). Calling reloadSections instead of just reloadData seems to help avoid that problem.
Faced same issue but the reason with me was that I forgot to connect the collection view Data Source to view controller
It is because the [cell count] don't equal the [real index count] + [insert indexes].
Sometimes the dispatch_async don't include block of the array insert data and insertItemsAtIndexPaths.
I got the problem with somtimes crash. It is not cause each time crash.
Just a guess: at the time you are inserting the first image, the collection view may not yet have loaded its data. However, in the exception message, the collection view claims to "know" the number of items before the insertion (1). Therefore, it could have lazily loaded its data in insertItemsAtIndexPaths: and taken the result as "before" state. Also, you don't need to reload data after an insertion.
Long story short, move the
[cv reloadData];
up to get
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
[cv reloadData];
}