Re-use/Leak of Decoration view in UICollectionViewFlowLayout Subclass - ios

Context:
I have already done the following
1. Watched the relevant videos for UICollectionView from WWDC 2012. Unfortunately they do not discuss much about the decoration views.
2. Implemented a collection view with decoration view. I subclassed the UICollectionViewFlowLayout to include the decoration view.
3. I have read the UICollectionView programming guide from Apple.
4. I understand that the Decoration views are controlled solely by the Layout object.
Problem:
1. When I analyze the code with Instruments I find that my decoration views are causing a memory leak.
2. On further analysis of the code I found that the decoration view is not being reused as I would expect. Every time the decoration view is needed a new one is created.
3. When the collection view deallocs only the last created decoration view gets dellocated and all other decorations views leak.
4. I do not understand how the decoration view will be reused when there is no deque method for it.
Questions:
1. Do we have to manually manage the removal and addition of decoration views from the collection view?
I am confused as to how is the decoration view works in general. Any pointers for this would help. The code for my layout object is as below. In the code I am simply putting a 10 points wide bar at the top of my collection view. Everytime the bar is scrolled off the screen and then brought back into the screen, a new decoration view is allocated. When the collection view is deallocated only the last allocated decoration view gets deallocated. Rest all leak.
#import "CollectionViewFlowLayoutSubclass.h"
#import "CollectionViewDecorationView.h"
#define DECORATION_VIEW_KIND #"DecorationViewShelf"
#implementation CollectionViewFlowLayoutSubclass
- (void)awakeFromNib
{
[super awakeFromNib];
[self registerClass:[CollectionViewDecorationView class] forDecorationViewOfKind:DECORATION_VIEW_KIND];
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (CGSize)collectionViewContentSize
{
return [super collectionViewContentSize];
}
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray* larrayAttributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray* larrayMutableAttributes = [larrayAttributes mutableCopy];
CGRect lrectFrame = CGRectMake(0, 0, 320, 10);
if (CGRectIntersectsRect(rect, lrectFrame))
{
UICollectionViewLayoutAttributes* lobjLayoutAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:DECORATION_VIEW_KIND
withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
lobjLayoutAttributes.frame = CGRectMake(0, 0, 320, 10);
[larrayMutableAttributes addObject:lobjLayoutAttributes];
}
else
{
NSLog(#"Rect %# does not intersect %#", NSStringFromCGRect(rect), NSStringFromCGRect(lrectFrame));
}
return [NSArray arrayWithArray:larrayMutableAttributes];
}
#end

Related

UITableView in UIStackView - smooth expand and collapse animation

I am trying to implement a table view that allows the user to "show more" rows to toggle the full number of rows or a smaller number of rows. I have all the data upfront when the view is loaded, so I do not need to fetch any extra data.
I am able to get it working, but the problem I am having is that my table view does not smoothly animate when expanding or collapsing. What happens is that if the "show more" action is triggered, the size of the table is updated all at once to the full height of the table with all the data in it. Then the rows will animate.
Likewise when hiding rows, the table height will shrink all at once to the end height and then the rows will animate.
Here is a picture of what is happening. It just "jumps" to the full height and then it animates the rows. But what I would like to happen would be for it to smoothly expand the height of the table unveiling the data as it smoothly expands downward. I would like the opposite, where it smoothly slides upward when pressing "show less".
The way my app is laid out is as follows:
UIScrollView
UIStackView
UIViewController
UIViewController
UITableView <-- the section I am working on here
UIView
...
Basically, I have a scrollable list of sections of different data. Some sections are tables, some are just ad-hoc views arranged with AutoLayout, others have collection views or page view controllers or other types of views.
But this section is represented by the UITableView in the previous list.
#import "MyTableView.h"
#import "MyAutosizingTableView.h"
#interface MyTableView ()
#property (strong, nonatomic) UILabel *titleLabel;
#property (strong, nonatomic) MyAutosizingTableView *tableView;
#end
#implementation MyTableView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
[self.titleLabel setText:#"Details"];
self.numberOfRows = 3;
[self addSubview:self.titleLabel];
[self addSubview:self.tableView];
[NSLayoutConstraint activateConstraints:#[
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor],
[self.titleLabel.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor],
[self.tableView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.tableView.topAnchor constraintEqualToSystemSpacingBelowAnchor:self.titleLabel.bottomAnchor multiplier:1.0f],
[self.tableView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[self.tableView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.tableView.widthAnchor constraintEqualToAnchor:self.widthAnchor]
]];
}
- (UITableView *)tableView {
if (!self->_tableView) {
self->_tableView = [[MyAutosizingTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self->_tableView.translatesAutoresizingMaskIntoConstraints = NO;
self->_tableView.rowHeight = UITableViewAutomaticDimension;
self->_tableView.estimatedRowHeight = UITableViewAutomaticDimension;
self->_tableView.allowsSelection = NO;
self->_tableView.scrollEnabled = NO;
self->_tableView.delegate = self;
self->_tableView.dataSource = self;
[self->_tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:#"dataCell"];
}
return self->_tableView;
}
- (UILabel *)titleLabel {
if (!self->_titleLabel) {
self->_titleLabel = [[UILabel alloc] init];
self->_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self->_titleLabel.numberOfLines = 0;
self->_titleLabel.userInteractionEnabled = YES;
self->_titleLabel.textAlignment = NSTextAlignmentNatural;
self->_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self->_titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
self->_titleLabel.adjustsFontSizeToFitWidth = NO;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showMore)];
[self->_titleLabel addGestureRecognizer:tapGesture];
}
return self->_titleLabel;
}
- (void)showMore {
if (self.numberOfRows == 0) {
self.numberOfRows = 3;
} else {
self.numberOfRows = 0;
}
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:indexes withRowAnimation:UITableViewRowAnimationFade];
}
- (void)setData:(NSArray<NSString *> *)data {
self->_data = data;
[self.tableView reloadData];
}
- (void)didMoveToSuperview {
//this has no effect unless the view is already in the view hierarchy
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.numberOfRows <= 0) {
return self.data.count;
}
return MIN(self.data.count, self.numberOfRows);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *data = self.data[indexPath.row];
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"dataCell" forIndexPath:indexPath];
cell.data = data;
return cell;
}
#end
I have a UITableView subclass that I used in the previous code. The reason I have the table view subclass is to get the table to resize when the content changes. If anyone knows a better way to do this without the subclass, I would be very interested in learning that too.
#import "MyAutosizingTableView.h"
#implementation MyAutosizingTableView
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
#end
Does anyone know what I can do to get smooth expansion animations? I would really like the table to expand smoothly and reveal the data that is in the new rows.
Update - a high level overview of what I am trying to do
Here I am going to try to describe what it is I am trying to accomplish and how I am going about it, so if anyone has any alternative approaches or improvements for laying this out, I would be very open to suggestions. Thank you in advance for reading this.
So I am working on a view controller that displays data about a product, imagine a car for example. The view controller has different sections responsible for displaying different data. For example, the first section displays pictures of the item and for this section, I am using a UIPageView that displays basically a carousel of product images.
Another section displays options about the current product. For example, someone might choose a different color for the car or different wheels. For this section, I am using a table view to display the list of available attributes, where each attribute is in a different section of the table. Upon pressing the section header, the table view adds a row that displays the available options for that attribute. For instance, assume the car has different colors available (red, green, blue, and yellow). Upon pressing the section header, the table view adds a row and animates that in (using the technique discussed in chapter 8 of "Programming iOS 12"). The row that is shown then contains a UICollectionView that scrolls horizontally allowing the user to choose between the colors (or wheel options or whatever attribute is being changed).
Another section displays what other customers have said about this product. For example, if someone leaves a writeup on the car, then that would go in this section. This section is has a table showing how the product fares across different criteria. Using the car example still, it might have a row for comfort and another for gas mileage and another for performance and so on. Then there is also a horizontal UICollectionView displaying the write ups about the product.
Yet another section is a list of attributes for the product. For example, with the car it might show engine size on the left and V12 on the right.
And there are other sections as well, but then to tie them all together I have a scroll view with a vertical UIStackView inside it. And this is what I am really curious about. What is the best way to display or layout all these sections? Is a stack view the way to go or should I use a table view or should I just have a scroll view with these views hooked together just using AutoLayout directly or is there some better way to do it than all of these?
One thing that is very important is that each section can be different sizes between products. For example, some sections might be one height for one product but then a completely different height for other products. So I need it to be able to dynamically size each section and I also need to be able to animate some size changes (for example, the section where it adds a row to the table and then removes it, that needs to animate and make the whole section larger in an animated fashion).
Also, I would like each section to be modular and to not end up having a giant view controller that manages everything. I would like each section to essentially be self contained. I just pass data to that section and then it handles everything related to layout and interaction. But again, the real issue is getting those views to be able to resize and to have that update in the parent view (stack view currently).
But I am really new to iOS development, and while I have been trying to learn as fast as I can, I am still not sure if the way that I am doing this currently (the scroll view with the stack view inside it) is the best approach or if there are better approaches. I tried this initially in a table view with static cells where each row was a different section. But then getting the child view controllers to resize the row in the parent table view was not working that well. Since then I have also changed from making each section be a separate view controller and just have each section be a view (rather than view controller) to make resizing hopefully easier, but I am still running into issues. I have also considered collection views, but I also need to be able to support iOS 12 (or I could drop iOS 12 support if I really need to and just support iOS 13+).
But to restate, my overall goals are to have some way to layout this interface which consists of different views that each handle different data. I would like each view to be able to resize and for that to be able to be smooth. And I would like each section of the interface to be modular so as to avoid one large view controller.
Update 2
After following the suggestion to wrap the table view in a UIView and then set the two bottom constraints with different priorities, this is what I am getting. I also get this effect anytime I try to animate things like the height constraint of views. I think this is because the overall view here is contained inside a stack view, so I think the stack view is doing something when its arranged subview changes size.
The picture here shows both expanding the table view and collapsing it. When it is collapsed it has three rows visible and when it is expanded it has five rows.
The yellow and blue views are representative of other views on the page. They are not part of this view and neither is the stack view they are contained within. They are just other parts of the page included here to show this issue. All the views here are contained in a stack view and I think that is what is causing the issue with the animation, but I am not sure how to fix it.
You really have a bunch of questions here - but to address specifically the issue with trying to expand/collapse your table view...
I think you'll be fighting a losing battle with this approach. By trying to use an "auto-sizing" table view, you're counter-acting much of a table view's behaviors/ And, since you are not getting the benefit of memory management with reusable cells, you're probably better off formatting your "spec list" with a vertical stack view.
In either case, to create a collapse / expand animation with a "reveal / hide" effect, you may want to embed the tableView or stackView in a "container" view, and then toggle constraint priorities for the height (the bottom) of the container.
Here is a quick example...
create a "container" UIView
add a vertical stack view to the container
add labels to the stack view
add a red UIView to place above the container
add a blue UIView to place below the container
define constraints for the height of the container
On each tap, the container will expand / collapse to reveal / hide the labels.
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
#interface ViewController ()
#property (strong, nonatomic) NSLayoutConstraint *collapsedConstraint;
#property (strong, nonatomic) NSLayoutConstraint *expandedConstraint;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIStackView *labelsStackView = [UIStackView new];
labelsStackView.axis = UILayoutConstraintAxisVertical;
labelsStackView.spacing = 4;
// let's add 12 labels to the stack view (12 "rows")
for (int i = 1; i < 12; i++) {
UILabel *v = [UILabel new];
v.text = [NSString stringWithFormat:#"Label %d", i];
[labelsStackView addArrangedSubview:v];
}
// add a view to show above the stack view
UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
// add a view to show below the stack view
UIView *blueView = [UIView new];
blueView.backgroundColor = [UIColor blueColor];
// add a "container" view for the stack view
UIView *cView = [UIView new];
// clip the container's subviews
cView.clipsToBounds = YES;
for (UIView *v in #[redView, cView, blueView]) {
v.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:v];
}
// add the stackView to the container
labelsStackView.translatesAutoresizingMaskIntoConstraints = NO;
[cView addSubview:labelsStackView];
// constraints
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// when expanded, we'll use the full height of the stack view
_expandedConstraint = [cView.bottomAnchor constraintEqualToAnchor:labelsStackView.bottomAnchor];
// when collapsed, we'll use the bottom of the 3rd label in the stack view
UILabel *v = labelsStackView.arrangedSubviews[2];
_collapsedConstraint = [cView.bottomAnchor constraintEqualToAnchor:v.bottomAnchor];
// start collapsed
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:#[
// redView Top / Leading / Trailing / Height=120
[redView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
[redView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[redView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[redView.heightAnchor constraintEqualToConstant:120.0],
// container Top==redView.bottom / Leading / Trailing / no height
[cView.topAnchor constraintEqualToAnchor:redView.bottomAnchor constant:0.0],
[cView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[cView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
// blueView Top==stackView.bottom / Leading / Trailing / Height=160
[blueView.topAnchor constraintEqualToAnchor:cView.bottomAnchor constant:0.0],
[blueView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[blueView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[blueView.heightAnchor constraintEqualToConstant:160.0],
// stackView Top / Leading / Trailing
[labelsStackView.topAnchor constraintEqualToAnchor:cView.topAnchor],
[labelsStackView.leadingAnchor constraintEqualToAnchor:cView.leadingAnchor],
[labelsStackView.trailingAnchor constraintEqualToAnchor:cView.trailingAnchor],
_expandedConstraint,
_collapsedConstraint,
]];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// toggle priority between expanded / collapsed constraints
if (_expandedConstraint.priority == UILayoutPriorityDefaultHigh) {
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
} else {
_collapsedConstraint.priority = UILayoutPriorityDefaultLow;
_expandedConstraint.priority = UILayoutPriorityDefaultHigh;
}
// animate the change
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}];
}
#end
You may be able to implement that with your "auto-sizing" table view, but again, since you're not using the "normal" table view behaviors (scrolling, reusable cells, etc), this might be a better approach.
Hopefully, it can also give you some insight to sizing other views in your layout - particularly if you want to animate them in or out of view.
Edit - added a more complex example here: https://github.com/DonMag/Scratch2021
It uses a vertical stack view in a vertical scroll view as the "main view" UI, adding child view controllers as "components" in the stack view.

Multiple supplementary views in a UICollectionViewFlowLayout?

I've got a fairly standard grid layout using a subclass of UICollectionViewFlowLayout. When the user taps a cell I move the cells in the subsequent rows down to make space for a detail view that will appear. It looks like this:
grid mockup
In that detail view I want to show another view with related data, similar to how iTunes shows album details. The existing layout has headers for each section and I'm currently slapping the detail view into place, manually managing the frame's position. This gets tricky with rotations and cells moving around.
How can I convince the layout to handle the detail's position by treating it as a supplementary view? My controller is configured correctly to display the detail as a supplementary view, same for the layout.
Solved the problem. In broad strokes, here's what works for me:
Create a subclass of UICollectionViewLayoutAttributes and register it in your layout by overriding the layoutAttributesClass function.
In layoutAttributesForElementsInRect: retrieve all standard layout attributes with [super layoutAttributesForElementsInRect:rect];.
Copy that array into a mutable array and append another set of attributes for the supplementary view doing something like [attributesCopy addObject:[self layoutAttributesForSupplementaryViewOfKind:YourSupplementaryKind atIndexPath:indexPathForTappedCell]];
In layoutAttributesForSupplementaryViewOfKind:atIndexPath: get the attributes for your view with something like
YourLayoutAttributes *attributes = [YourLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
Test that the elementKind matches the type of supplementary view you want to generate
Retrieve the cell's layout attributes using [self layoutAttributesForItemAtIndexPath:indexPath];
Change the frame from the cell's attributes to suit the needs of your supplementary view
Assign the new frame to the supplementary attributes
The important part to note is you can't skip subclassing UICollectionViewLayoutAttributes. Doing asking super (a UICollectionViewFlowLayout instance) for attributes of a supplemental view for anything but standard header or footers will return nil. I couldn't find any concrete documentation on this behavior so I might be wrong but in my experience it was the subclassed attributes that solved my problems.
Your code should look something like this:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allAttributesInRect = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *attributes = NSMutableArray.array;
for (UICollectionViewLayoutAttributes *cellAttributes in allAttributesInRect)
{
// Do things with regular cells and supplemental views
}
if (self.selectedCellPath)
{
UICollectionViewLayoutAttributes *detailAttributes = [self layoutAttributesForSupplementaryViewOfKind:SomeLayoutSupplimentaryDetailView atIndexPath:self.selectedCellPath];
[attributes addObject:detailAttributes];
}
return attributes.copy;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
SomeLayoutAttributes *attributes = [SomeLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
if ([elementKind isEqualToString:SomeLayoutSupplimentaryDetailView])
{
UICollectionViewLayoutAttributes *cellAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
CGRect frame = cellAttributes.frame;
frame.size.width = CGRectGetWidth(self.collectionView.frame); // or whatever works for you
attributes.frame = frame;
}
return attributes;
}
Your UICollectionViewLayoutAttributes subclass doesn't necessarily need any extra properties or functions but it's a great place to store data specific to that view for configuration use after you retrieve the view using dequeueReusableSupplementaryViewOfKind:forIndexPath:.

Adjusting Container View's Height to Match Embedded UITableView

Using Storyboards and Autolayout, I have a UIViewController with a UIScrollView as the main view. I have several container views embedded in the scroll view. Some of those embedded container views contain UITableViews, each having cells of different heights. I'll need the tableView's height to be large enough to show all cells at once, as scrolling will be disabled on the tableView.
In the main UIViewController, container view's height has to be defined in order for the scroll view to work properly. This is problematic because there's no way for me to know how large my tableView will be once all it's cells of varying heights are finished rendering. How can I adjust my container view's height at runtime to fit my non-scrolling UITableView?
So far, I've done the following:
// in embedded UITableViewController
//
- (void)viewDidLoad {
// force layout early so I can determine my table's height
[self.tableView layoutIfNeeded];
if (self.detailsDelegate) {
[self.detailsTableDelegate didDetermineHeightForDetailsTableView:self.tableView];
}
}
// in my main UIViewController
// I have an IBOutlet to a height constraint set up on my container view
// this initial height constraint is just temporary, and will be overridden
// once this delegate method is called
- (void)didDetermineHeightForDetailsTableView:(UITableView *)tableView
{
self.detailsContainerHeightConstraint.constant = tableView.contentSize.height;
}
This is working fine and I was pleased with the results. However, I have one or two more container views to add, which will have non-scrolling tableViews, and I'd hate to have to create a new delegate protocol for each container view. I don't think I can make the protocol I have generic.
Any ideas?
Here's what I ended up doing:
// In my embedded UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 60.0;
// via storyboards, this viewController has been embeded in a containerView, which is
// in a scrollView, which demands a height constraint. some rows from our static tableView
// might not display (for lack of data), so we need to send our table's height. we'll force
// layout early so we can get our size, and then pass it up to our delegate so it can set
// the containerView's heightConstraint.
[self.tableView layoutIfNeeded];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// in another embedded view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// then, in the parent view controller, I do this:
// 1) ensure each container view in the storyboard has an outlet to a height constraint
// 2) add this:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.placeDetailsContainerHeightConstraint.constant = self.placeDetailsTableViewController.sizeForEmbeddingInContainerView.height;
self.secondaryExperiencesContainerHeightConstraint.constant = self.secondaryExperiencesViewController.sizeForEmbeddingInContainerView.height;
}
I haven't done this yet, but it'd probably be best to create a Protocol with a property of CGSize sizeForEmbeddingInContainerView that each child view controller can adopt.
Here's what worked for me perfectly.
- (void)updateSizeBasedOnChildViews {
// Set height of container to match embedded tableview
CGRect containerFrame = self.cardTableContainer.frame;
containerFrame.size.height = [[[self.cardTableContainer subviews] lastObject]contentSize].height;
self.cardTableContainer.frame = containerFrame;
// Set content height of scrollview according to container
CGRect scrollFrame = self.cardTabScrollView.frame;
scrollFrame.size.height = containerFrame.origin.y + containerFrame.size.height;
// + height of any other subviews below the container
self.cardTabScrollView.contentSize = scrollFrame.size;
}

UICollectionView custom layout with two cells

I've been reading online tutorials on UICollectionView with different layouts. Also looked at a lot of SO Questions on the subject. But it seems what I am looking might be something more simple but I am stuck on how to go forward.
The Goal
I have a UIViewController that is embedded in a UINavigation controller. I am displaying data in a UITableView which includes:1 UIImageView and three UILabels in each cell. The data is fetched from a server and all works nicely.
I then wanted to have a UIButton that, when tapped, would kick off a cool animation that shows the cells transition into a nice grid view.
It suddenly dawned on me that I needed to use a UICollectionView to change between these two cells and ditch the UITableView completely. Tapping the button again, would switch back to the last state (Grid or UITableView style)
The grid cell needs to loose one label - but keep the image.
The problem
I have spent the last two days reading up on UICollectionView and UICollectionViewFlowLayout. I think I could use a Apple's pre-made UICollectionViewFlowLayout and just tweak it a little.
I don't know if I need two custom cells or one cell that changes shape between the two views and how the animations must work.
I'm not looking for the exact code to do this - I just need to know which direction I need to go in and if I need to use two custom cells - and how do I change between the two with animation and not reloading all the data again.
Appreciate any input.
Thanks all.
I finally found a solution that was acceptable to my need. If anyone ever has similar needs - this is how you use two different custom UICollectionViewCell's and how to change between the two different cells / layouts.
First thing is create the customCells in IB - creating the xib
files.
Then set the up as you need
Since my requirement needed the standard flow layout provided by the class UICollectionViewFlowLayout - I just needed to create two custom layouts and tweak them to my needs.
Create two (or more if needed) classes that subclass UICollectionViewFlowLayout
In the implementation - setup the layout as needed. Since I am subclassing the pre-made UICollectionViewFlowLayOut and all I need to do is tweak it - the implementation is pretty simple.
So - for the table view layout I did this:
tableViewFlowLayOut.m
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(320, 80);
self.minimumLineSpacing = 0.1f;
}
return self;
}
This sets each cells width and height to the values I needed. self.minimumLineSpacing sets the spacing between the cells. (Spacing between the cell above / below )
Then for the grid layout:
gridFlowLayOut.m
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(159, 200);
self.minimumInteritemSpacing = 0.1f;
self.minimumLineSpacing = 0.1f;
}
return self;
}
Same as before - however, this time I needed spacing between my cells right edge -
self.minimumInteritemSpacing = 0.1f'
takes care of that.
Right - now to put it all together - in the viewController that has the UICollectionView
viewController.m
// Import the new layouts needed.
#import "GridFlowLayOut.h"
#import "TableViewFlowLayOut.m"
//Create the properties
#property (strong, nonatomic) TableViewFlowLayOut *tableViewLayout;
#property (strong, nonatomic) GridFlowLayOut *grideLayout;
-(void)viewDidLow
{
//Register the two custom collection view cells you created earlier. Make sure you set the correct reuse identifier here.
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"TableViewCell" bundle:nil] forCellWithReuseIdentifier:#"TableItemCell"];
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"GridViewCell" bundle:nil] forCellWithReuseIdentifier:#"GridItemCell"];
}
-(void)viewWillAppear
{
//Create the layout objects
self.grideLayout = [[GridFlowLayOut alloc]init];
self.tableViewLayout = [[TableViewFlowLayOut alloc]init];
//Set the first layout to what it should be
[self.tradeFeedCollectionView setCollectionViewLayout:self.tableViewLayout];
}
Right - now to change between the layouts with some animation. This is actually very easy to do and only needs a few lines of code -
I called this code in a button method in viewController.m
-(void)changeViewLayoutButtonPressed
{
//BOOl value to switch between layouts
self.changeLayout = !self.changeLayout;
if (self.changeLayout){
[self.tradeFeedCollectionView setCollectionViewLayout:self.grideLayout animated:YES];
}
else {
[self.tradeFeedCollectionView setCollectionViewLayout:self.tableViewLayout animated:YES];
}
}
And lastly in cellForItemAtIndexPath
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *tableCellIdentifier = #"TableItemCell";
static NSString *gridCellIdentifier = #"GridItemCell";
//BOOL used to detect which layout is active
if (self.gridLayoutActive == NO){
CustomCollectionCellClass *tableItemCell = [collectionView dequeueReusableCellWithReuseIdentifier:tableCellIdentifier forIndexPath:indexPath];
//Setup the cell
}
return tableItemCell;
}else
{
CustomCollectionCellClass *gridItemCell= [collectionView dequeueReusableCellWithReuseIdentifier:gridCellIdentifier forIndexPath:indexPath];
//Setup the cell
}
return gridItemCell;
}
return nil;
}
Of course you will need to conform to the other UICollectionView delegates and setup the remaining stuff.
This actually took me a while to figure out. I really hope it helps others out there.
If anyone wants a demo project - I'll happily create one and upload to GitHub.
For anyone new to UICollectionViews I highly recommend reading Apple's programming guide on the subject - it was this document which lead me to this solution.
Reference:
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/Introduction/Introduction.html

UICollectionView not removing old cells after scroll

I have a UICollectionView with a grid of images. When you tap on one, it opens up the grid and shows a subview with some details. Like this:
I open up the grid in my UICollectionViewLayout by adjusting the UICollectionViewLayoutAttributes and setting a translation on the transform3D property for all cells below the current row of the selected item. This works really nicely, and is a much better animation and a simpler approach than my first attempt at inserting another cell into the grid which is a different size to the others.
Anyway... it works most of the time, but then after continued use I see old images on the collection view. They are like ghost cells. I can't click them, it's like they haven't been removed from the collection view properly, and they sit on top of the cells preventing taps and just being a nuisance. Like this:
Any ideas why these cells are doing this?
EDIT:
I'd like to add, I think it only happens when i scroll the collection view really fast. I've written my own UICollectionViewFlowLayout replacement to test if it still happens. It does.
EDIT 2:
The 3d transforms or layout have nothing to do with this. It must be a bug in UICollectionView. I can exploit by just scrolling really fast, letting come to a standstill and then querying the views that are on screen. There are often double the number of cells, but they are hidden as they are stacked on top of each other. My implementation above reveals them because of the translation i do.
This can really hurt performance too.
See my answer for a solution.
My second edit of my question details why this is happenening, and here is my workaround. It's not bullet proof, but it works in my case, and if you experience somethign similar you could tweak my solution:
- (void) removeNaughtyLingeringCells {
// 1. Find the visible cells
NSArray *visibleCells = self.collectionView.visibleCells;
//NSLog(#"We have %i visible cells", visibleCells.count);
// 2. Find the visible rect of the collection view on screen now
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
//NSLog(#"Rect %#", NSStringFromCGRect(visibleRect));
// 3. Find the subviews that shouldn't be there and remove them
//NSLog(#"We have %i subviews", self.collectionView.subviews.count);
for (UIView *aView in [self.collectionView subviews]) {
if ([aView isKindOfClass:UICollectionViewCell.class]) {
CGPoint origin = aView.frame.origin;
if(CGRectContainsPoint(visibleRect, origin)) {
if (![visibleCells containsObject:aView]) {
[aView removeFromSuperview];
}
}
}
}
//NSLog(#"%i views shouldn't be there", viewsShouldntBeThere.count);
// 4. Refresh the collection view display
[self.collectionView setNeedsDisplay];
}
and
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self removeNaughtyLingeringCells];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self removeNaughtyLingeringCells];
}
A quick further comment to bandejapaisa's: under iOS 6 only, I found that UICollectionView also had a habit of bungling animated transitions. The original cells would remain where they were, copies would be made and then the copies would be animated. Usually on top of the originals but not always. So a simple bounds test wasn't sufficient.
I therefore wrote a custom subclass of UICollectionView that does the following:
- (void)didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
//
// iOS 6 contains a bug whereby it fails to remove subviews, ever as far as I can make out.
// This is a workaround for that. So, if this is iOS 6...
//
if(![UIViewController instancesRespondToSelector:#selector(automaticallyAdjustsScrollViewInsets)])
{
// ... then we'll want to wait until visibleCells has definitely been updated ...
dispatch_async(dispatch_get_main_queue(),
^{
// ... then we'll manually remove anything that's a sub of UICollectionViewCell
// and isn't currently listed as a visible cell
NSArray *visibleCells = self.visibleCells;
for(UIView *view in self.subviews)
{
if([view isKindOfClass:[UICollectionViewCell class]] && ![visibleCells containsObject:view])
[view removeFromSuperview];
}
});
}
}
Obviously it's a shame that 'is this iOS 6' test can't be a little more direct but it's hidden off in a category in my actual code.
A Swift UICollectionView extension version of bandejapaisa's answer:
extension UICollectionView {
func removeNaughtyLingeringCells() {
// 1. Find the visible cells
let visibleCells = self.visibleCells()
//NSLog("We have %i visible cells", visibleCells.count)
// 2. Find the visible rect of the collection view on screen now
let visibleRect = CGRectOffset(bounds, contentOffset.x, contentOffset.y)
//NSLog("Rect %#", NSStringFromCGRect(visibleRect))
// 3. Find the subviews that shouldn't be there and remove them
//NSLog("We have %i subviews", subviews.count)
for aView in subviews {
if let aCollectionViewCell = aView as? UICollectionViewCell {
let origin = aView.frame.origin
if (CGRectContainsPoint(visibleRect, origin)) {
if (!visibleCells.contains(aCollectionViewCell)) {
aView.removeFromSuperview()
}
}
}
}
// 4. Refresh the collection view display
setNeedsDisplay()
}
}

Resources