Faster UICollectionView cells - ios

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

Related

How to load UICollectionView cells in the background

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.

Custom UICollectionViewCell increases memory allocation

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.

UICollectionView datasource methods not getting called, but are being set in the init

Here is my source code
- (id)initWithCollectionView:(UICollectionView *)collectionView
{
self = [super init];
if (self)
{
self.collectionView = collectionView;
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
[self.collectionView registerClass:[TWNTweetCell class] forCellWithReuseIdentifier:kCell];
self.collectionViewLayout = self.collectionView.collectionViewLayout;
self.tweetArray = #[];
self.tweetTextArray = #[];
self.twitter = [STTwitterAPI twitterAPIOSWithFirstAccount];
}
return self;
}
#pragma mark - CollectionView
#pragma mark DataSource
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
return [self.tweetArray count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TWNTweetCell *cell = (TWNTweetCell *)[collectionView dequeueReusableCellWithReuseIdentifier:kCell forIndexPath:indexPath];
NSDictionary *status = [self.tweetArray objectAtIndex:indexPath.row];
NSString *text = [status valueForKey:#"text"];
cell.usernameLabel.text = screenName;
// cell.createdAtLabel.text = dateString;
cell.autoresizingMask = UIViewAutoresizingFlexibleWidth;
UITextView *textView = [self.tweetTextArray objectAtIndex:indexPath.row];
[cell setTweet:text withTweetTextView:textView];
return cell;
}
All the methods don't get interupted at all by breakpoints. The tweets are getting loaded in the log so I know everything else is ok, its just not recognizing the collection view. And yes i've set the
Anyone have any idea whats going on?
It is not your case, it might be helpful for others who will came here having problem with data source methods not being called. It could be assigning data source like:
collectionView.dataSource = MyDataSource()
which is wrong as dataSource is a weak reference so it needs to be stored by some strong reference to be alive after creating it. Added a private property in a ViewController to keep the strong reference, initialising and then assigning it fixes the issue.
A few suggestions:
Do all your UICollectionView setup and configuration in viewDidLoad.
Ensure you calling the create init method from your other class
Your tweetArray is also empty, so if the number of items method is called, it will return nothing and the other methods will not be called
A couple things:
1) in (and only in) your "init" method, use the underlying instance variable for your #property. That is,
_collectionView = collectionView;
_collectionView.dataSource = self;
_collectionView.delegate = self;
This is called "direct access", and more information can be seen in this related question.
2) in your .h file, make certain to declare that your object conforms to the data source & delegate protocols. E.G.
#interface JustinViewController : UIViewController <UICollectionViewDelegate, UICollectionViewDataSource>
for swift do this , set a property
//MARK: props
let dataSource = MyDataSource()
and in
viewDidLoad(){
// your other code
..
..
collectionView.dataSource = dataSource // it is a strong reference
}
apart form these other general pitfall are
not returning the count or the data source
not populating the data source
Add the collectionView to a view hierarchy.
In the init method you set the property (self.collectionView) but you do not add the collectionView to a view hierarchy. So the collectionView won't call any dataSource or delegate method.
I created collection view in storyboard and linked datasource and delegate but they were not being called in Xcode 8.0 with Swift 3.0. Tried multiple things but the solution was to declare the delegate and datasource in class declaration line:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
...
}
Previously when we linked delegate and datasource through storyboard it was not required, may be a bug :)
Call [collectionView reloadData] at the end of your init method. The collection view needs to be told to populate itself. I assume UICollectionViewController does this internally, but you don't seem to be using UICollectionViewController (or at least not in the usual way).

Setting the background for a selected UITableViewCell in a grouped UITableView

This is working fine for my plain style table views, but not for my grouped style. I'm trying to customize how the cell looks when it is selected.
Here is my code:
+ (void)customizeBackgroundForSelectedCell:(UITableViewCell *)cell {
UIImage *image = [UIImage imageNamed:#"ipad-list-item-selected.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
cell.selectedBackgroundView = imageView;
}
I have verified that the correct cell is indeed being passed into this function. What do I need to do differently to make this work?
It's not clear from your question whether or not you're aware that the tableViewCell automatically manages showing/hiding it's selectedBackgroundView based on its selection state. There are much better places to put that method other than in viewWillAppear. One would be at the time you initially create the tableViewCells, i.e.:
- (UITableViewCell *)tableView:(UITV*)tv cellForRowAtIP:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
cell = [tv dequeueCellWithIdentifier:#"SomeIdentifier"];
if (cell == nil) {
cell = /* alloc init the cell with the right reuse identifier*/;
[SomeClass customizeBackgroundForSelectedCell:cell];
}
return cell;
}
You only need to set the selectedBackgroundView property once in the lifetime of that cell. The cell will manage showing/hiding it when appropriate.
Another, cleaner, technique is to subclass UITableViewCell, and in the .m file for your subclass, override:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithBla....];
if (self) {
UIImageView *selectedBGImageView = /* create your selected image view */;
self.selectedBackgroundView = selectedBGImageView;
}
return self;
}
From then on out your cell should show it's custom selected background without any further modifications. It just works.
Furthermore, this method works better with the current recommended practice of registering table view cell classes with the table view in viewDidLoad: using the following UITableView method:
- (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier
You would use this method in your table view controller's viewDidLoad method, so that your table view cell dequeuing implementation is much shorter and easier to read:
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[SomeClass class]
forCellReuseIdentifier:#"Blah"];
}
- (UITableViewCell *)tableView:(UITV*)tv cellForRowAtIP:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"Blah"
forIndexPath:indexPath];
/* set your cell properties */
return cell;
}
This method is guaranteed to return a cell as long as you have registered a class with the #"Blah" identifier.

Why am I getting a deallocated memory call when I call UICollectionViewCell dequeueReusableCellWithReuseIdentifier?

I have a UICollectionView that contains custom UICollectionViewCells (TestReceiptCell is the class name).
I was not having any problems getting the UICollectionView to appear and load the custom cells when the custom cells only contained a UILabel.
I then added a UITableView via IB into the TestReceiptCell NIB file. I set a referencing outlet in TestReceiptCell.h for the UITableView and synthesized in the .m file. I set the delegate and datasource for the UITableView to the ViewController containing the UICollectionView.
Now when running the app I get a EXC_ BAD_ ACCESS exception in this block on the third line:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"TestReceiptCell";
TestReceiptCell *cell = (TestReceiptCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; //exception thrown here
return cell;
}
I ran the Zombie Instrument test and found that the deallocated memory call originates here. This is my first time using that instrument so I am not exactly sure how to investigate from here.
For reference, here are some more relevant parts of the code:
ViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self.myCollectionView registerNib:[UINib nibWithNibName:#"TestReceiptCell" bundle:nil] forCellWithReuseIdentifier:#"TestReceiptCell"];
// Setup flowlayout
myCollectionViewFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[myCollectionViewFlowLayout setItemSize:CGSizeMake(310, 410)];
[myCollectionViewFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[self.myCollectionView setCollectionViewLayout:myCollectionViewFlowLayout];
self.myCollectionView.pagingEnabled = YES;
}
I am implementing the UITableView datasource and delegate methods in the ViewController.m file as well but I am not sure if the problem lies here given the origination of the EXC_BAD_ACCESS exception:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"eventCell"];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"eventCell"];
}
return cell;
}
UPDATE:
I am able to get this to run if I change cellForItemAtIndexPath to:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"TestReceiptCell";
//TestReceiptCell *cell = (TestReceiptCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
TestReceiptCell *cell = [NSBundle.mainBundle loadNibNamed:#"TestReceiptCell" owner:self options:nil][0];
return cell;
}
However, I am not dequeuing cells and know this is not the correct way. There seems to be an issue somewhere in the initWithFrame method that gets called when dequeueReusableCellWithResueIdentifier creates a new cell. Here is that method currently:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:#"TestReceiptCell" owner:self options:nil];
if ([arrayOfViews count] < 1) {
return nil;
}
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
return nil;
}
self = [arrayOfViews objectAtIndex:0];
}
return self;
}
EDIT:
If I do not select a delegate or a datasource for the tableview, the collectionview with tableviews will load. Something in attaching the delegate/datasource to File's Owner is causing the error.
When you register a UINib for cell reuse, dequeueReusableCellWithReuseIdentifier:forIndexPath: is what calls instantiateWithOwner:options: on the UINib that you registered. Whatever it passes for owner, is what becomes the File's Owner outlet in your nib.
It appears that you are expecting the File's Owner to be the UICollectionView, but I don't think that it is.
Even if it were, I don't think you should use the UICollectionView for the delegate of the UITableView contained within each collection cell. That would require your UICollectionView to keep track of the tableViews and contents within each cell.
I'd suggest setting the delegate of the contained tableView to the collection cell itself and have each cell manage its own tableview.
EDIT:
You can define a delegate protocol for your collection view cells to communicate the relevant table view events to the collection view. With this approach, you would set the delegate property you define for each collection cell in the collectionView:cellForItemAtIndexPath method of your collection view datasource.
When the user, for example, selects an item from the table, you can call the cell delegate to inform the collection view which item was selected.
This approach allows you to abstract the fact that your collection cell is using a table view to display the cell information. Later, if you decide you want to use, for example, an embedded UICollectionView to display those items, the delegate protocol can remain unchanged and you can isolate your changes to the collection cell.

Resources