UICollectionView can't collapse or remove supplementary view - ios

I have a uicollectionview using flow layout that has a supplementary header view which is a view that I only sometimes want to display. So basically I want to have a button that will, when clicked, remove the supplementary view from the collection view and also re-place all the items in the collection view with the consideration that the header is gone. Is this possible? I've tried it repeatedly in many ways. Changing the reference header size, changing my answer to the delegate method for the header size, invalidating the layout, reloading the data etc etc etc. What am I missing?
I just ran a test. I think that it's related to using UIDynamics, what is it in UIDynamics that would override my delegate response for header section reference size?

In your delegate's layout methods return the appropriate size for whatever state you're interested in:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
if (self.headerVisble) {
return CGSizeMake(collectionView.bounds.size.width, 30.0f);
}
else {
return CGSizeZero;
}
}
Then when you need to update the layout call:
[collectionView.collectionViewLayout invalidateLayout];
The collection view will ask the Layout object for new info which will in turn ask your delegate. I believe it will animate the change too.

Related

UICollectionView doesn't reuse cell

I'm trying to imitate UITableView layout using UICollectionView.
layout.itemSize = CGSizeMake(CGRectGetWidth(self.view.bounds), 44.0f);
I register the reusable cell class.
[self.collectionView registerClass:[SampleCell class]
forCellWithReuseIdentifier:NSStringFromClass([SampleCell class])];
Note: SampleClass is just a subclass of UICollectionViewCell which contains nothing.
And conformed to the data source:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 28;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([SampleCell class])
forIndexPath:indexPath];
return cell;
}
I found that the SampleCell is not reused. To validate it, we can simply log the number of subviews in the UICollectionView.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"number of subviews in collection view is: %li", (long)self.collectionView.subviews.count);
}
And after scrolling, I got this log:
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
Notice that there are 30 subviews (2 of those are the scrollview indicator).
That means that all of the 28 items are displayed without the invisible cells removed from superview. Why does this happen?
To make it easier for you, I made a sample project available on Github.
https://github.com/edwardanthony/UICollectionViewBug
Update:
I also checked the memory allocation using the memory graph hierarchy debugger and it's allocated 28 times.
I does work, it's just keeping a little more in memory due to more aggressive caching. If you try changing the number of items from 28 to 100 you will see that it stays at 33 subviews when you scroll.
Try adding the following code to your SampleCell class and you will see it gets called, but maybe not quite as you expect.
- (void)prepareForReuse {
[super prepareForReuse];
NSLog(#"prepareForReuse called");
}
UICollectionView has a more advanced caching scheme than UITableView (or at least as it used to have), which is the reason you see what you do. According to docs it says Cell prefetching is enabled by default:
UICollectionView provides two prefetching techniques you can use to
improve responsiveness:
Cell prefetching prepares cells in advance of
the time they are required. When a collection view requires a large
number of cells simultaneously—for example, a new row of cells in grid
layout—the cells are requested earlier than the time required for
display. Cell rendering is therefore spread across multiple layout
passes, resulting in a smoother scrolling experience. Cell prefetching
is enabled by default.
Data prefetching provides a mechanism whereby
you are notified of the data requirements of a collection view in
advance of the requests for cells. This is useful if the content of
your cells relies on an expensive data loading process, such as a
network request. Assign an object that conforms to the
UICollectionViewDataSourcePrefetching protocol to the
prefetchDataSource property to receive notifications of when to
prefetch data for cells.
You can turn off cell prefetching by adding this line to setupCollectionView function in your sample:
self.collectionView.prefetchingEnabled = NO;
Doing so will make your sample work as you expected. The subview count will drop to 18 in my case.
I suspect that counting subviews does not reflect cell reuse. It could be that subviews contains more than one reference to the same cell. To count the number of cells used, you could log how many times the UICollectionViewCell subclass gets initialised. Just override it's init method and put a print statement in there.
One other thing to note (sorry if it's aleady obvious), if all cells are visible on screen no reuse will occur. Cell reuse occurs when cells go off screen during scrolling.

UIcollection view showing the images stacked in a single view

One of my storyboard has a table view cell which calls the collection view defined in the separate scene. Collection view includes a UIImage view with a page control till ios 10 each of the images used to display fully within the imageview after the upgrade all the images are showing up as stacked on one another as in the attached image.once i click on this stacked image everything becomes proper until i revisit the storyboard. Is there anything which changed with respect to UIImage or collectionview in the new upgrade?
The same code works fine in ipod.
Code Sample:
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return collectionView.bounds.size;
}
Is it something to do with pagecontrol being used??
I hope you would have used sizeForItemAtIndexPath for specifying collection view cell size previously and would have added UICollectionViewDataSource, UICollectionViewDelegate delegate methods.
Now for iOS 10 we need to add UICollectionViewDelegateFlowLayout delegate, only then sizeForItemAtIndexPath will be executed.
Try it I hope it will fix your issue.
Try explicitly providing the height and width of collectionView.bounds.size

Deleting cell at edge of UICollectionView - cells not appearing immediately after scroll

Consider an standard, vertically scrolling flow layout populated with enough cells to cause scrolling. When scrolled to the bottom, if you delete an item such that the content size of the collection view must shrink to accommodate the new number of items (i.e. delete the last item on the bottom row), the row of cells that scroll in from the top are hidden. At the end of the deletion animation, the top row appears without animation - it's a very unpleasant effect.
In slow motion:
It's really simple to reproduce:
Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController
Add a UICollectionViewController to the storyboard that uses a standard flow layout, and change its class to ViewController. Give the cell prototype the identifier "Cell" and a size of 200x200.
Add the following code to ViewController.m:
#interface ViewController ()
#property(nonatomic, assign) NSInteger numberOfItems;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.numberOfItems = 19;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.numberOfItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
self.numberOfItems--;
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
#end
Additional Info
I've seen other manifestations of this problem when dealing with collection views, it's just that the above example seems the simplest to demonstrate the issue. UICollectionView seems to go into some kind of paralysed state of panic during the default animations, and refuses to unhide certain cells until after the animation completes. It even prevents manual calls to cell.hidden = NO on hidden cells from having an effect (hidden is still YES afterwards). Dropping down to the underlying layer and setting hidden there works, provided you can get a reference to the cell you want to unhide, which is non-trivial when dealing with cells that haven't been displayed yet.
-initialLayoutAttributesForAppearingItemAtIndexPath is being called for every item visible at the time of the call to deleteItemsAtIndexPaths:, but not for the ones that are scrolled into view. It is possible work around the issue by calling reloadData inside a batch update block immediately afterwards, which appears to make the collection view realise that the top row is about to appear:
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
[collectionView performBatchUpdates:^{
[collectionView reloadData];
} completion:nil];
But unfortunately this is not an option for me. I am trying to implement some custom animation timing by manipulating the cell layers & animations, and calling reloadData really throws things out of whack by causing unnecessary layout callbacks.
Update: A bit of investigation
I added log statements to a lot of layout methods and looked through some stack frames to try and find out what's going wrong. Crucially, I'm checking when layoutSubviews is called, when the collection view asks for layout attributes from the layout object (layoutAttributesForElementsInRect:) and when applyLayoutAttributes: is called on the cells.
I would expect to see a sequence of methods like this:
// user taps cell (to delete it)
-deleteItemsAtIndexPaths:
-layoutAttributesForElementsInRect:
-finalLayoutAttributes...: // Called for the item being deleted
-finalLayoutAttributes...: // \__ Called for each index path visible
-initialLayoutAttributes...: // / when deletion started
-applyLayoutAttributes: // Called for the item being deleted, to apply final layout attributes
// collection view begins scrolling up
-layoutSubviews: // Called multiple times as the
-layoutAttributesForElementsInRect: // collection view scrolls
// ... for any new set of
// ... attributes returned:
-collectionView:cellForItemAtIndexPath:
-applyLayoutAttributes: // Sets the standard attributes for the new cell
// collection view finishes scrolling
Most of this is happening; layout is correctly triggered as the view scrolls, and the collection view properly queries the layout for the attributes of cells to be displayed. However, collectionView:cellForItemAtIndexPath: and the corresponding applyLayoutAttributes: methods are not being called until after the deletion, when layout is invoked one last time causing the hidden cells to be assigned their layout attributes (sets hidden = NO).
So it seems that despite receiving all the correct responses from the layout object, the collection view has some kind of flag set to not update the cells during the update. There is a private method on UICollectionView called from within layoutSubviews that seems responsible for refreshing the cells' appearance: _updateVisibleCellsNow:. This is from where the data source eventually gets asked for a new cell before applying the cells starting attributes, and it seems this is the point of failure, as it is not being called when it should be.
Additionally, this does seem to be related to the update animation, or at least cells are not updated for the duration of the insertion/deletion. For example the following works without glitches:
- (void)addCell
{
NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForItem:self.numberOfItems
inSection:0];
self.numberOfItems++;
[self.collectionView insertItemsAtIndexPaths:#[indexPathToInsert]];
[self.collectionView scrollToItemAtIndexPath:indexPathToInsert
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
If the above method is called to insert a cell while the inserted cell is outside the current visible bounds, the item is inserted without animation and the collection view scrolls to it, properly dequeuing and displaying cells on the way.
Problem occurs in iOS 7 & iOS 8 beta 5.
Adjust your content insets so that they go beyond the bounds of the device's screen size slightly.
collectionView.contentInsets = UIEdgeInsetsMake(-5,0,0,0); //Adjust this value until it looks ok

UICollectionViewCell and Autolayout scaling

I've built a vanilla UICollectionView that uses cells that are the full size of my screen...
The ViewCells I am using are defined in IB with dimensions of 320x568, and I have added auto layout constraints that should allow these to scale down when the window size changes.
Is there a way to have a normal collection view flow layout detect changes in the available window (in-call bar, 3.5 inch screen, etc.), and update the viewcell default sizes so that they match the available real estate, or is this going to require customized code to listen for the change in resolution? If so, what is the method I need to implement to capture this change in window size?
This might work :
Subclass a UICollectionViewFlowLayout and override the method (shouldInvalidateLayoutForBoundsChange) like this :
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
[self invalidateLayout];
return YES;
}
This will force a refresh of the layout on bounds changes of the collectionView. (a rotation for example).
also you'll have to implement the following method of UICollectionViewDelegate :
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.collectionView.bounds.size;
}
Now if you have good constraints on you collectionsView, it's frame should be updated on window changes and result in a layout update.
Let me know if it works :)

setNeedsLayout relayouts the cell only after is has been displayed

I have a table view with cells, which sometimes have an optional UI element, and sometimes it has to be removed.
Depending on the element, label is resized.
When cell is initialised, it is narrower than it will be later on. When I set data into the label, this code is called from cellForRowAtIndexPath:
if (someFlag) {
// This causes layout to be invalidated
[flagIcon removeFromSuperview];
[cell setNeedsLayout];
}
After that, cell is returned to the table view, and it is displayed. However, the text label at that point has adjusted its width, but not height. Height gets adjusted after a second or so, and the jerk is clearly visible when all cells are already displayed.
Important note, this is only during initial creation of the first few cells. Once they are reused, all is fine, as optional view is removed and label is already sized correctly form previous usages.
Why isn't cell re-layouted fully after setNeedsLayout but before it has been displayed? Shouldn't UIKit check invalid layouts before display?
If I do
if (someFlag) {
[flagIcon removeFromSuperview];
[cell layoutIfNeeded];
}
all gets adjusted at once, but it seems like an incorrect way to write code, I feel I am missing something else.
Some more code on how cell is created:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ProfileCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
[cell setData:model.items[indexPath.row] forMyself:YES];
return cell;
}
// And in ProfileCell:
- (void)setData:(Entity *)data forMyself:(BOOL)forMe
{
self.entity = data;
[self.problematicLabel setText:data.attributedBody];
// Set data in other subviews as well
if (forMe) {
// This causes layouts to be invalidated, and problematicLabel should resize
[self.reportButton removeFromSuperview];
[self layoutIfNeeded];
}
}
Also, if it matters, in storyboard cell looks like this, with optional constraint taking over once flag icon is removed:
I agree that calling layoutIfNeeded seems wrong, even though it works in your case. But I doubt that you're missing something. Although I haven't done any research on the manner, in my experience using Auto Layout in table cells that undergo a dynamic layout is a bit buggy. That is, I see herky jerky layouts when removing or adding subviews to cells at runtime.
If you're looking for an alternative strategy (using Auto Layout), you could subclass UITableViewCell and override layoutSubviews. The custom table cell could expose a flag in its public API that could be set in the implementation of tableView:cellForRowAtIndexPath:. The cell's layoutSubviews method would use the flag to determine whether or not it should include the optional UI element. I make no guarantees that this will eliminate the problem however.
A second strategy is to design two separate cell types and swap between the two in tableView:cellForRowAtIndexPath: as necessary.
You've added additional code to the question, so I have another suggestion. In the cell's setData:forMyself: method, try calling setNeedsUpdateConstraints instead of layoutIfNeeded.

Resources