I have a collection view that displays instances of CatViewController views:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CatCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCatCellId forIndexPath:indexPath];
[cell updateCat:[self.dataSource catAtIndex:indexPath.row]]; //catAtIndex will return a Cat Core Data object
return cell;
}
CatCell looks like this:
- (void)prepareForReuse {
[super prepareForReuse];
[self.catVC.view removeFromSuperview];
self.catVC = nil;
}
- (void)updateCat:(Cat*)cat {
self.catVC = [[CatViewController alloc] initWithNibName:nil bundle:nil];
self.catVC.view.frame = self.bounds;
[self.contentView addSubview:self.catVC.view];
self.catVC.cat = cat;
}
self.catVC.cat is what causes the CatViewController to configure itself with all the view data associated with a Cat object. The problem is that when the UICollectionView scrolls, it pauses briefly as the new CatViewController is created and displayed. Obviously I want the collection view to be completely smooth, and have the view for each cell appear when it's ready, without blocking the main thread.
This is easy and well-documented to do with images, but I'm struggling to do the same with a view controller's view.
Reuse as much of your controller and view infrastructure as possible. There is a reason that table and collection views offer cell reuse - tear down and recreation is expensive.
Collection view memory management is the art of reuse. By not reusing the controller and it's view you are subverting the cell reuse (because you destroy and recreate 90% of the cell content each time.
Related
I have a UICollectionView that uses a couple of different layouts depending on user preferences. I want certain cells to show up for some layouts but not for others. How do I accomplish this? Do I actually need to reload the collection view data?
There are two steps to accomplishing this (which also works with UITableView).
Update the data source. If you're using an NSDictionary or NSArray, then you'll need to add or remove the items you want to show/hide.
Call reloadData on the UICollectionView or UITableView. That's pretty much it.
If you want to remove or add with an animation, that's different. There are a couple more methods in the middle that you have to call and make sure that your update sequence is correct. But that is a different question altogether.
EDIT:
As an example of how to use an array
- (void)methodCalledWhenLayoutChanges:(BOOL)includeOptionalString {
if (includeOptionalString) {
[_collectionViewDataSourceArray addObject:_optionalString];
} else {
[_collectionViewDataSourceArray removeObject:_optionalString];
}
[self.collectionView reloadData];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _collectionViewDataSourceArray.count;
}
// Never actually setup a collection view like this. This is just an example of how to reference a data source for creating a collection view cell.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [[UICollectionViewCell alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 50.0f, 50.0f)];
UILabel *textLabel = [[UILabel alloc] initWithFrame:cell.bounds];
textLabel.text = [_collectionViewDataSourceArray objectAtIndex:indexPath.row];
[cell addSubview:textLabel];
return cell;
}
There are a lot of answers out there for loading images into UITableViews or UICollectionViews. But what if my UICollectionView is displaying views from other view controllers?
Say in my UICollectionViewCell subclass I have this:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.catViewController = [[CatViewController alloc] initWithNibName:nil bundle:nil];
self.catViewController.view.frame = self.bounds;
[self addSubview:self.catViewController.view];
}
return self;
}
And in my collection view Datasource:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSomeId forIndexPath:indexPath];
cell.catViewController.data = self.data[indexPath.row]; //see below
return cell;
}
The catViewController has a setter for the data property. When this property is set, the cat will load it's image, along with some other related images for that view. So how do I properly reuse the MyCell cells so that the collection view doesn't stutter each time it creates (or reuses) a cell? Each MyCell takes up the full width of the collection view, which scrolls horizontally, so every time a new cell scrolls into view, the collection view stalls for a moment.
For High performance CollectionView cells , Use following new stuffs in iOS10 with xcode8
Implement protocol "UICollectionViewDataSourcePrefetching" in you ViewController as
class ViewController: UIViewController , UICollectionViewDataSourcePrefetching {
Set following delegates to your collection view in storyboard (see the attached image)
or programmatically
In ViewController's viewDidLoad method
collectionView.delegate = self
collectionView.dataSource = self
collectionView.prefetchDataSource = self
After finishing my app, I realized that the memory allocation is incredibly huge.
I think I have isolated the problem to a view which makes use of a UICollectionView.
The collection view has custom cell.
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 12;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCollectionCell *yearCell = [collectionView dequeueReusableCellWithReuseIdentifier:myCellIdentifier forIndexPath:indexPath];
if (yearCell == nil)
yearCell = [[AgendaYearCollectionCell alloc] init];
yearCell.layer.shouldRasterize = YES;
yearCell.layer.rasterizationScale = [UIScreen mainScreen].scale;
[yearCell setCurrentDate:newDate];
return yearCell;
}
I registered the nib of the custom cell in viewDidLoad:
UINib * nib = [UINib nibWithNibName:#"AgendaYearCollectionCell" bundle:[NSBundle mainBundle]];
[self.collectionView registerNib:nib forCellWithReuseIdentifier:myCellIdentifier];
MyCollectionViewCell is a custom (inherited) UICollectionViewCell and its setCurrentDate method does:
-(void)setCurrentDate:(NSDate *)date
{
if (calendar == nil)
calendar = [[myCalendarView alloc] initWithDate:currentMonth];
[self.contentView addSubview:calendar];
calendar = nil;
[self setNeedsDisplay];
}
The problem is that memory increase linearly as I add/remove new cell to the view.
I was supposing that dequeueReusableCellWithReuseIdentifier does what I need: reuse cells keeping memory usage low.
But this does not happen. For instance, my collection view is a calendar: a grid of 12 months. Therefore, I need always 12 and only 12 cells.
There is a way for a better management of the collection ?
I set my reuse cell identifier here
EDIT:
I think here is your problem, you add calendar each time in collection view delegate,so you just reused your MyCollectionCell but your calendar in MyCollectionCell is not reused.that's why you can see the memory print grow. So , you should make MyCalendarView more reusable so that you don't have to alloc it each time.
-(void)setCurrentDate:(NSDate *)date
{
if (calendar == nil){
calendar = [[myCalendarView alloc] initWithDate:currentMonth];
[self.contentView addSubview:calendar];
// calendar = nil;//here you dealloc calendar which make `if(calendar == nil)` run each time.
[self setNeedsDisplay];
}
}// each calendar in Collection Cell won't be create or refresh again.
Don't init CollectionViewCell like UITabelView cell in cellForItemAtIndexPath. Delete all of these code
// if (yearCell == nil)
// yearCell = [[AgendaYearCollectionCell alloc] init];
// yearCell.layer.shouldRasterize = YES;
// yearCell.layer.rasterizationScale = [UIScreen mainScreen].scale;
Because your cell is Nib cell, bring all these setting into awakeFromNib in MyCollectionCell.m like this
-(void)awakeFromNib{
[super awakeFromNib];
self.layer.shouldRasterize = YES;
}
registerNib is enough in the case of Nib cell, remember specify your MyCollectionCell in Nib cell class.
Use Instruments for analysing your app's memory footprint. In XCode > Product > Profile and select Leaks on Instruments. It is very very useful to trace high memory usage responsible calls.
I'm swapping out the data being displayed in my collection view by changing the datasource. This is being done as part of a tab-like interface. When the new data loads, I would like to flash the scroll indicators to tell the user that there's more data outside of the viewport.
Immediately
Doing so immediately doesn't work because the collection view hasn't loaded the data yet:
collectionView.dataSource = dataSource2;
[collectionView flashScrollIndicators]; // dataSource2 isn't loaded yet
dispatch_async
Dispatching the flashScrollIndicators call later doesn't work either:
collectionView.dataSource = dataSource2;
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView flashScrollIndicators]; // dataSource2 still isn't loaded
});
performSelector:withObject:afterDelay:
Executing the flashScrollIndicators after a timed delay does work (I saw it somewhere else on SO), but leads to a bit of lag with the scroll indicators being shown. I could decrease the delay, but it seems like it'll just leads to a race condition:
collectionView.dataSource = dataSource2;
[collectionView performSelector:#selector(flashScrollIndicators) withObject:nil afterDelay:0.5];
Is there a callback that I can hook on to to flash the scroll indicators as soon as the collection view has picked up on the new data and resized the content view?
Subclassing UICollectionView and overriding layoutSubviews can be a solution. You can call [self flashScrollIndicators] on the collection. Problem is that layoutSubviews gets called in multiple scenarios.
Initially when collection is created and datasource is assigned.
On scrolling, cells which go beyond the viewport get re-used & re-layout.
Explicitly change frame/reload the collection.
Workaround to this can be, keeping a BOOL property which will be made YES only when reloading datasource, otherwise will remain NO. Thus flashing of scroll bars will happen explicitly only when reloading collection.
In terms of source code,
MyCollection.h
#import <UIKit/UIKit.h>
#interface MyCollection : UICollectionView
#property (nonatomic,assign) BOOL reloadFlag;
#end
MyCollection.m
#import "MyCollection.h"
#implementation MyCollection
- (void) layoutSubviews {
[super layoutSubviews];
if(_reloadFlag) {
[self flashScrollIndicators];
_reloadFlag=NO;
}
}
Usage should be
self.collection.reloadFlag = YES;
self.collection.dataSource = self;
Put your call to flashScrollIndicators inside UICollectionViewLayout's method -finalizeCollectionViewUpdates.
From Apple's documentation:
"... This method is called within the animation block used to perform all of the insertion, deletion, and move animations so you can create additional animations using this method as needed. Otherwise, you can use it to perform any last minute tasks associated with managing your layout object’s state information."
Hope this helps!
Edit:
Ok, I got it. Since you mentioned the finalizeCollectionViewUpdates method was not being called I decided to try it myself. And you're right. The problem is (sorry I didn't notice this earlier) that method is only called after you update the Collection View (insert, delete, move a cell, for example). So in this case it doesn't work for you. So, I have a new solution; it involves using UICollectionView's method indexPathsForVisibleItems inside UICollectionViewDataSource's method collectionView:cellForItemAtIndexPath:
Every time you hand a new UICollectionViewCell to your collection view, check if it is the last of the visible cells by using [[self.collectionView indexPathsForVisibleItems] lastObject]. You will also need a BOOL ivar to decide if you should flash the indicators. Every time you change your dataSource set the flag to YES.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
NSIndexPath *iP = [[self.collectionView indexPathsForVisibleItems] lastObject];
if (iP.section == indexPath.section && iP.row == indexPath.row && self.flashScrollIndicators) {
self.flashScrollIndicators = NO;
[self.collectionView flashScrollIndicators];
}
return cell;
}
I tried this approach and it's working for me.
Hope it helps!
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];
}