Change insets for each section in a UICollectionView - objective-c

I have a simple UICollectionView written in Swift. My sectionInsets used to be 0, 35, 0, 35 which gave the entire collectionView margins left end right. My minimumLineSpacing is set to default 10, which means all of my cells have a margin of 10 between each other. All good.
Then I divided the UICollectionView into two sections. The first section serves to hold a cell that acts as a prompt to create a new cell (which will end up in section 2). The second section holds all other cells (the actual content).
My problem is that ever since I added the second section, the spacing between the one static cell in section 1 and the first cell in section 2 isn't 10 anymore but 35+10. I'm now trying to get this particular spacing back to 10.
My thought was to add an if condition to identify whether it's section 1 or 2 and set the sectionInsets accordingly. I'm stuck however as to where and how exactly I should do this. I don't seem to be able to call sectionInsets or FlowLayout from my View Controller at e.g. cellForItemAtIndexPath. Should I do it in my CustomCollectionViewFlowLayout subclass? Here is the code that I have in there.
- (void)awakeFromNib
{
self.itemSize = CGSizeMake(305.0, 407.0);
self.minimumInteritemSpacing = 20.0;
self.minimumLineSpacing = 10.0;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(0, 35, 0, 35);
}
And this is the code for my sections in my view controller:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let firstCell = collectionView.dequeueReusableCellWithReuseIdentifier("createCell", forIndexPath: indexPath) as! CreateCollectionViewCell
firstCell.imageView.image = UIImage(named: "puppy3")
return firstCell
} else {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("mainCell", forIndexPath: indexPath) as! MainCollectionViewCell
cell.imageView?.image = self.imageArray[indexPath.row]
return cell
}
}
UPDATE:
I figured it out. It works for me by adding two variables for sectionInsets of type UIEdgeInsets in my view controller and then adding this function:
//Set custom insets per section
func collectionView(collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
if section == 0 {
return sectionInsets1
} else {
return sectionInsets2
}
}

Related

How to configure spacing in a collection view cell that is the same as the leading edge with the navigation bar title?

I have a UICollectionViewController where I want to display collection view cells where the left edge is aligned with the title of the navigation bar. I certainly don't want to hardcode the spacing of CGFloat 20 due to various device screen sizes. Given from my code below, the implementation looks fine on an iPhone 8+ and iPhone 11 Pro Max, but not the iPhone 11 (the collection view cell left edge is more to the right than the edge of the navigation bar title, as shown from the image below my code).
Does anyone know of a method to programmatically (or through Storyboards) align the leading edge of both the navigation bar title and collection view cell?
private let reuseIdentifier = "Cell"
class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
private let spacing: CGFloat = 20 // CGFloat of 20 works well for the iPhone 8+ and iPhone 11 Pro Max, but not the iPhone 11 Pro
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
// Setup layout
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
layout.minimumLineSpacing = spacing
layout.minimumInteritemSpacing = spacing
self.collectionView?.collectionViewLayout = layout
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 16
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = .systemFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberOfItemsPerRow: CGFloat = 2
let spacingBetweenCells: CGFloat = spacing
let totalSpacing = (2 * self.spacing) + ((numberOfItemsPerRow - 1) * spacingBetweenCells) // Amount of total spacing in a row
if let collection = self.collectionView {
let width = (collection.bounds.width - totalSpacing) / numberOfItemsPerRow
return CGSize(width: width, height: width)
} else {
return CGSize(width: 0, height: 0)
}
}
}
I thought of a neat solution. Maybe a workaround but I am satisfied.
// Change private let spacing: CGFloat = 20 to
private var spacing = CGFloat()
// Upon loading, spacing will be initialized depending on screen width
override func viewDidLoad() {
super.viewDidLoad()
// Added code
if UIScreen.main.bounds.width <= 375 {
spacing = 16 // For the iPhone 7, 8, 11 Pro
} else {
spacing = 20 // For the iPhone 7+, 8+ and 11 Pro Max
}
}

UICollectionView deselectItem when cell pre-fetching is enabled

On iOS 10.0, UICollectionView pre-fetches cells by default. This leads to cells that are prepared for being shown on screen, but are hidden. This question describes it really well.
The following code will successfully deselect an index path when its cell is either visible or does not exist at all. If the cell exists and is hidden, the index path will be deselected, but the cell becomes stuck in the selected state until it is reused.
collectionView!.deselectItem(at: indexPath, animated: false)
This problem does not exits on iOS 9 or when pre-fetching is disabled with isPrefetchingEnabled = false on iOS 10.0.
Is this a bug in UICollectionView or am I misunderstanding how deselectItem is supposed to work?
Here is the full code of a UICollectionViewController subclass that demonstrates this behaviour with the following steps:
Tap on a cell, so that it becomes selected (red)
Scroll the cell slightly off-screen
Tap the "Deselect Cell" button
Scroll the cell back on screen
Observe how it still looks selected
Tap on another cell
Observe how both cells look selected
Scroll the first cell far off-screen and back again
Observe how the first cell finally does not look selected
import UIKit
private let reuseIdentifier = "Cell"
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
let button = UIButton(frame: CGRect(x: 10, y: 30, width: 360, height: 44))
button.backgroundColor = #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)
button.setTitleColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1), for: .normal)
button.setTitleColor(#colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1), for: .highlighted)
button.setTitle("Deselect Cell", for: .normal)
button.addTarget(self, action: #selector(CollectionViewController.buttonPress), for: .touchUpInside)
view.addSubview(button)
}
func buttonPress() {
for indexPath in collectionView!.indexPathsForSelectedItems ?? [IndexPath]() {
let cell = collectionView!.cellForItem(at: indexPath)
NSLog("Deselecting indexPath: %#, cell: %#", indexPath.description, cell?.frame.debugDescription ?? "not visible")
collectionView!.deselectItem(at: indexPath, animated: false)
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 300
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
cell.selectedBackgroundView = UIView(frame: cell.bounds)
cell.selectedBackgroundView!.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
return cell
}
}
As far as I can tell this is a bug in UICollectionView, and I've opened a Radar myself. You might want to do so as well to apply more pressure.
Even before prefetching, collection view didn't bother deselecting cells that weren't visible. Even cellForItemAtIndexPath: states that it returns nil for cells that are not visible.
But before prefetching, as soon as a cell left the content view it was added to the reuse pool, and when you scrolled back you got a reused cell that had its selected state reset.
Now, that cell remains loaded and wont reset until it's reused, by scrolling further away, beyond the prefetching area.
To fix this you can either set prefetchingEnabled to NO and lose its benefits, or update selection state whenever a cell appears -
- (void)collectionView:(UICollectionView *)collectionView
willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
if (!cell.isSelected) {
return;
}
if ([[collectionView indexPathsForSelectedItems] containsObject:indexPath]) {
return;
}
cell.selected = NO;
}
This doesn't seem to diminish performance.

the behavior of the UICollectionViewFlowLayout is not defined, because the cell width is greater than collectionView width

2015-08-18 16:07:51.523 Example[16070:269647] the behavior of the
UICollectionViewFlowLayout is not defined because: 2015-08-18
16:07:51.523
Example[16070:269647] the item width must be less than
the width of the UICollectionView minus the section insets left and
right values, minus the content insets left and right values.
2015-08-18 16:07:51.524 Example[16070:269647] The relevant
UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout:
0x7f24d670>, and it is attached to <UICollectionView: 0x7b377200;
frame = (0 0; 768 1024); clipsToBounds = YES; autoresize = W+H;
gestureRecognizers = <NSArray: 0x7f24ce30>; animations = {
position=<CABasicAnimation: 0x7f279d60>;
bounds.origin=<CABasicAnimation: 0x7f279430>;
bounds.size=<CABasicAnimation: 0x7f279480>; }; layer = <CALayer:
0x7aec9cf0>; contentOffset: {0, 0}; contentSize: {1024, 770}>
collection view layout: <UICollectionViewFlowLayout: 0x7f24d670>.
2015-08-18 16:07:51.524 Example[16070:269647] Make a symbolic
breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch
this in the debugger.
That is what I get and what I do
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(self.collectionView!.frame.size.width - 20, 66)
}
I rotate from landscape to portrait then the console shows this error message only in iOS 9 if anyone knows what happens and if there is a fix for this?
select collectionview in storyboard go to size inspector change estimate size to none
This happens when your collection view resizes to something less wide (go from landscape to portrait mode, for example), and the cell becomes too large to fit.
Why is the cell becoming too large, as the collection view flow layout should be called and return a suitable size ?
collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath)
Update to include Swift 4
#objc override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{ ... }
This is because this function is not called, or at least not straight away.
What happens is that your collection view flow layout subclass does not override the shouldInvalidateLayoutForBoundsChange function, which returns false by default.
When this method returns false, the collection view first tries to go with the current cell size, detects a problem (which logs the warning) and then calls the flow layout to resize the cell.
This means 2 things :
1 - The warning in itself is not harmful
2 - You can get rid of it by simply overriding the shouldInvalidateLayoutForBoundsChange function to return true.
In that case, the flow layout will always be called when the collection view bounds change.
XCode 11
For me, the reason was that the CollectionView's Estimate option size was set to Automatic.
It seems it is the default behavior in XCode 11.
Hence for me which I loaded the images in the cells, because of the size of the images and the automatic option (which wants to adapt the size of the cell with the size of the image) the cell's width became greater than the CollectionView's width.
By setting this option to None the problem solved.
I've also seen this occur when we set the estimatedItemSize to automaticSize and the computed size of the cells is less than 50x50 (Note: This answer was valid at the time of iOS 12. Maybe later versions have it fixed).
i.e. if you are declaring support for self-sizing cells by setting the estimated item size to the automaticSize constant:
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
and your cells are actually smaller than 50x50 points, then UIKit prints out these warnings. FWIW, the cells are eventually sized appropriately and the warnings seem to be harmless.
One fix workaround is to replace the constant with a 1x1 estimated item size:
flowLayout.estimatedItemSize = CGSize(width: 1, height: 1)
This does not impact the self-sizing support, because as the documentation for estimatedItemSize mentions:
Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method.
However, it might/might-not have performance implications.
I've solve this problem by using safeAreaLayoutGuide.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (view.safeAreaLayoutGuide.layoutFrame.width), height: 80);
}
and you also have to override this function to support portrait and landscape mode correctly.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout();
}
I am using a subclass of UICollectionViewFlowLayout and using its itemSize property to specify the cell size (instead of the collectionView:sizeForItemAtIndexPath: delegate method). Every time I rotate the screen to a shorter width one (e.g. landscape -> portrait) I get this huge warning in question.
I was able to fix it by doing 2 steps.
Step 1: In UICollectionViewFlowLayout subclass's prepareLayout() function, move super.prepareLayout() to after where self.itemSize is set. I think this makes the super class to use the correct itemSize value.
import UIKit
extension UICollectionViewFlowLayout {
var collectionViewWidthWithoutInsets: CGFloat {
get {
guard let collectionView = self.collectionView else { return 0 }
let collectionViewSize = collectionView.bounds.size
let widthWithoutInsets = collectionViewSize.width
- self.sectionInset.left - self.sectionInset.right
- collectionView.contentInset.left - collectionView.contentInset.right
return widthWithoutInsets
}
}
}
class StickyHeaderCollectionViewLayout: UICollectionViewFlowLayout {
// MARK: - Variables
let cellAspectRatio: CGFloat = 3/1
// MARK: - Layout
override func prepareLayout() {
self.scrollDirection = .Vertical
self.minimumInteritemSpacing = 1
self.minimumLineSpacing = 1
self.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: self.minimumLineSpacing, right: 0)
let collectionViewWidth = self.collectionView?.bounds.size.width ?? 0
self.headerReferenceSize = CGSize(width: collectionViewWidth, height: 40)
// cell size
let itemWidth = collectionViewWidthWithoutInsets
self.itemSize = CGSize(width: itemWidth, height: itemWidth/cellAspectRatio)
// Note: call super last if we set itemSize
super.prepareLayout()
}
// ...
}
Note that the above change will somehow make the layout size change when screen rotates stops working. This is where step 2 comes in.
Step 2: Put this in the view controller that holds the collection view.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
collectionView.collectionViewLayout.invalidateLayout()
}
Now the warning is gone :)
Some Notes:
Make sure you are adding constraints to the collectionView and not using collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
Make sure you are not calling invalidateLayout in viewWillTransitionToSize() because the width of an edge-to-edge cell in landscape is larger than the collection view’s frame width in portrait. See below references.
References
Making Adaptive Forms using UICollectionView
Collection view with self-sizing cells
For Xcode 11, none of the above methods worked for me.
It turned out one need to set the Estimate Size to None in the collection view size panel.
see: https://forums.developer.apple.com/thread/123625
Its happens because your collection view cell's width is bigger than collection view width after rotation.suppose that you have a 1024x768 screen and your collection view fills the screen.When your device is landscape,your cell's width will be self.collectionView.frame.size.width - 20 =1004 and its greater than your collection view's width in portrait = 768.The debugger says that "the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values".
This works for me:
Invalidate the layout on rotation:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
Have a separate function for calculating the available width:
Note: taking in to account both section inset and content inset.
// MARK: - Helpers
func calculateCellWidth(for collectionView: UICollectionView, section: Int) -> CGFloat {
var width = collectionView.frame.width
let contentInset = collectionView.contentInset
width = width - contentInset.left - contentInset.right
// Uncomment the following two lines if you're adjusting section insets
// let sectionInset = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, insetForSectionAt: section)
// width = width - sectionInset.left - sectionInset.right
// Uncomment if you are using the sectionInset property on flow layouts
// let sectionInset = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? UIEdgeInsets.zero
// width = width - sectionInset.left - sectionInset.right
return width
}
And then of course finally returning the item size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: calculateCellWidth(for: collectionView, section: indexPath.section), height: 60)
}
I think it is important to note that this works because invalidating a layout wont trigger a recalculation of the cell size immediately, but during the next layout update cycle. This means that once the item size callback is eventually called, the collection view has the correct frame, thus allowing accurate sizing.
Voila!
Turn your collection view's estimate size 'None' value from automatic.
You can do this by the storyboard or programatically.
But you have to calculate it manually with custom layout class or 'UICollectionViewDelegateFlowLayout' protocol APIs
After doing some experimenting, this seems to also be tied to how you layout your collectionView.
The tl;dr is: Use AutoLayout, not autoresizingMask.
So for the core of the problem the best solutions I've found to handle orientation change use the following code, which all makes sense:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
However, there are still situations where you can get the item size warning. For me it's if I am in landscape in one tab, switch to another tab, rotate to portrait, then return to the previous tab. I tried invalidating layout in willAppear, willLayout, all the usual suspects, but no luck. In fact, even if you call invalidateLayout before super.willAppear() you still get the warning.
And ultimately, this problem is tied to the size of the collectionView bounds updating before the delegate is asked for the itemSize.
So with that in mind I tried using AutoLayout instead of collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight], and with that, problem solved! (I use SnapKit to do AutoLayout so that I don't pull my hair out constantly). You also just need to invalidateLayout in viewWillTransition (without the coordinator.animate, so just as you have in your example), and also invalidateLayout at the bottom of viewWillAppear(:). That seems to cover all situations.
I dont know if you are using autoresizing or not - it would be interesting to know if my theory/solution works for everyone.
You can check in debugger if collectionView.contentOffset is changed to negative in my case it changes from (0,0) to (0,-40). You can solve this issue by using this method
if ([self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
self.automaticallyAdjustsScrollViewInsets = NO;
}
This solution definitely works.. Try this code
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
collectionView.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
I had a similar issue.
In my case, I had a collection view and when you tapped on one of the cells, a popover with a UITextField opened, to edit the item. After that popover disappeared, the self.collectionView.contentInset.bottom was set to 55 (originally 0).
To fix my issue, after the popover view disappears, I’m manually setting contentInset to UIEdgeInsetsZero.
The original issue seems to be related to the contextual prediction bar that shows up on top of the keyboard. When the keyboard is hidden, the bar disappears, but the contentInset.bottom value is not restored to the original value.
Since your issue seems to be related to the width and not to the height of the cell, check if any of the contentInset or layout.sectionInset values are the same as the one set by you.
I had the same issue. I fixed this by clearing my collection view of it's constraints, and resetting them in Storyboard.
I could solve it by putting
super.prepare()
at end of
override func prepare() { }
I've been struggling with this problem for some hours, too.
My UICollectionViewController is embedded in a container view, when rotating the iPad from landscape to portrait it showed the error. The cells did resize though.
I solved the error by calling the following in the UICollectionViewController:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
self.collectionView.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
The "super" has to be called after invalidating the layout.
Additionally I needed to call the following in the view where the container view is embedded in:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
DispatchQueue.main.async {
self.collectionViewController.collectionView.reloadData()
}
}
This way, the cells are updated after the screen rotation is done.
It is my solution:
Remove all UICollectionViewFlowDelegate methods from your view controller.
Create your own flowLayout class as a subclass UICollectionViewFlowLayout.
class ImageCollectionViewFlowLayout: UICollectionViewFlowLayout {}
Configure all sizes and other parameters into prepare() method. See my example below.
private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
self.sectionInset = sectionInsets
self.minimumLineSpacing = sectionInsets.bottom
self.minimumInteritemSpacing = sectionInsets.bottom
let width = collectionView.bounds
.inset(by: collectionView.layoutMargins)
.inset(by: self.sectionInset)
.width
self.estimatedItemSize = CGSize(width: width, height: width + 16)
//self.itemSize - I do not set itemsize because my cell has dynamical height
}
Set your new class for collection view on your storyboard(or xib).
That's all. Keep your code clean.
if you're a using Dynamic Types for fonts, just reload a collectionView when font size is changing:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
collectionView?.reloadData()
}
I found that this worked quite well for UICollectionViewController and animated correctly:
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
// Ensure the layout is within the allowed size before animating below, or
// `UICollectionViewFlowLayoutBreakForInvalidSizes` breakpoint will trigger
// and the logging will occur.
if ([self.collectionView.collectionViewLayout isKindOfClass:[YourLayoutSubclass class]]) {
[(YourLayoutSubclass *)self.collectionView.collectionViewLayout updateForWidth:size.width];
}
// Then, animate alongside the transition, as you would:
[coordinator animateAlongsideTransition:^
(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self.collectionView.collectionViewLayout invalidateLayout];
}
completion:nil];
[super viewWillTransitionToSize:size
withTransitionCoordinator:coordinator];
}
///////////////
#implementation YourLayoutSubclass
- (void)prepareLayout {
[super prepareLayout];
[self updateForWidth:self.collectionView.bounds.size.width];
}
- (void)updateForWidth:(CGFloat)width {
// Update layout as you wish...
}
#end
... See inline code comments above for explanation. 👍🏻
Any Changes in UIView must be done from main thread. Put your code of collectionView addition to views hierarchy into:
DispatchQueue.main.async {
...
}
In my case, for a vertical UICollectionView I had:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: padding(scale: 37))
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: padding(scale: 10), left: padding(scale: 4), bottom: padding(scale: 3), right: padding(scale: 4))
}
I fixed it by resting the horizontal insets to the size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (collectionView.bounds.width - (2 * padding(scale: 4))), height: padding(scale: 37))
}
In case you're setting the itemSize of collectionViewLayout in code, then make sure that the size is valid. Set a breakpoint on the code line where you set the itemSize, and check if the size complies to the instruction of the error warning:
The item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
This is what you need:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
//Get frame width
let width = self.view.frame.width
//I want a width of 418 and height of 274 (Aspect ratio 209:137) with a margin of 24 on each side of the cell (See insetForSectionAt 24 + 24 = 48). So, check if a have that much screen real estate.
if width > (418 + 48) {
//If I do return the size I want
return CGSize(width: 418, height: 274)
}else{
//Get new width. Frame width minus the margins I want to maintain
let newWidth = (width - 48)
//If not calculate the new height that maintains the aspect ratio I want. NewHeight = (originalHeight / originalWidth) * newWidth
let height = (274 / 418) * newWidth
//Return the new size that is Aspect ratio 209:137
return CGSize(width: newWidth, height: height)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 33
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 33
}

hidesBarsOnSwipe never shows navbar again when scrolling up

So I want to hide the navbar when scrolling down and bring it back when scrolling up. Hiding it works perfectly with
self.navigationController?.hidesBarsOnSwipe = true
But I expect it to be shown again when scrolling up. I made a test project where the view controller just has a single UICollectionView that covers the whole screen. Then showing the navbar is shown again as expected until I add this line to the viewDidLoad (adding cells to the collection view):
self.collectionView.delegate = self
And this is what the whole view controller looks like
class ViewController: UIViewController,UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "Test")
self.navigationController?.hidesBarsOnSwipe = true
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("Test", forIndexPath: indexPath) as UICollectionViewCell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(300, 300)
}
}
So why does showing the navbar stop working when I add cells to my collection view?
I had the same problem but with a web view.
The problem was that the top constraint of the web view was "Top Layout Guide.Top" , after changing the top constraint to "Superview.Top" the problem was solved.
To expand on Oleg's answer...
If you are using Interface Builder to set a constraint to a view controller's primary view, Xcode defaults to showing options to set the vertical constraint against the top layout guide. However, if you press 'Option', you will see an alternate set of constraints. The constraint for 'Top Space to Container' is what you're looking for.
I had same issue. When I added the code for hiding status bar along with navigation bar, it worked.
- (BOOL)prefersStatusBarHidden {
return self.navigationController.isNavigationBarHidden;
}
I tried setting hidesBarsOnSwipe property to true in my ViewController class in ViewDidLoad function as given below, but it didn't work in handling hiding the navigation bar on swipe-up and unhiding the navigation bar on swipe-down.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.hidesBarsOnSwipe = true
}
}
Setting hidesBarsOnSwipe to true will have effect only if we are using the UITableViewController or UICollectionViewController as main screens, hidesBarsOnSwipe will not work if we have added a UITableView to the UIViewController for displaying the list of data.
Solution
class TestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.hidesBarsOnSwipe = true
}
}
Hope this answer might help...!
I filed a bug report with Apple and ended up using AMScrollingNavbar instead which works really well and is easy to setup.
As per previous comments - this seems like a bug as of ios 10.3
as you are using a uicollectionview - I draw your attention to some code I re-wrote from APDynamicHeaderTableViewController
https://github.com/aaronpang/APDynamicHeaderTableViewController/issues/4
It's using snapkit https://github.com/SnapKit/SnapKit
(Apologies to all the IB + NSLayout Constraint lovers.)
class APDynamicHeaderTableViewController : UIViewController {
var largeWideSize = CGSize(width: UIScreen.main.bounds.width , height: 285 )
let headerView = APDynamicHeaderView () // Change your header view here
let cellLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
var feedCV:UICollectionView!
fileprivate var headerViewHeight:CGFloat = 80 // this will be updated by scrolling
fileprivate var headerBeganCollapsed = false
fileprivate var collapsedHeaderViewHeight : CGFloat = UIApplication.shared.statusBarFrame.height
fileprivate var expandedHeaderViewHeight : CGFloat = 100
fileprivate var headerExpandDelay : CGFloat = 100
fileprivate var tableViewScrollOffsetBeginDraggingY : CGFloat = 0.0
init(collapsedHeaderViewHeight : CGFloat, expandedHeaderViewHeight : CGFloat, headerExpandDelay :CGFloat) {
self.collapsedHeaderViewHeight = collapsedHeaderViewHeight
self.expandedHeaderViewHeight = expandedHeaderViewHeight
self.headerExpandDelay = headerExpandDelay
super.init(nibName: nil, bundle: nil)
}
init () {
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
self.view.backgroundColor = .green
// Cell Layout Sizes
cellLayout.scrollDirection = .vertical
cellLayout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
cellLayout.itemSize = CGSize(width: UIScreen.main.bounds.width, height: 185 + 80)
// Header view
self.view.addSubview(headerView)
headerView.snp.remakeConstraints { (make) -> Void in
make.top.left.equalToSuperview()
make.width.equalToSuperview()
make.height.equalTo(headerViewHeight)
}
// CollectionView
feedCV = UICollectionView(frame: .zero, collectionViewLayout: cellLayout)
self.view.addSubview(feedCV)
self.feedCV.snp.remakeConstraints { (make) -> Void in
make.top.equalTo(headerView.snp.bottom) // this is pegged to the header view which is going to grow in height
make.left.equalToSuperview()
make.width.equalToSuperview()
make.bottom.equalToSuperview()
}
feedCV.backgroundColor = .red
feedCV.showsVerticalScrollIndicator = true
feedCV.isScrollEnabled = true
feedCV.bounces = true
feedCV.delegate = self
feedCV.dataSource = self
// YOUR COLLECTIONVIEW CELL HERE!!!!!
feedCV.register(VideoCollectionViewCell.self, forCellWithReuseIdentifier: VideoCollectionViewCell.ID)
}
// Animate the header view to collapsed or expanded if it is dragged only partially
func animateHeaderViewHeight () -> Void {
Logger.verbose("animateHeaderViewHeight")
var headerViewHeightDestinationConstant : CGFloat = 0.0
if (headerViewHeight < ((expandedHeaderViewHeight - collapsedHeaderViewHeight) / 2.0 + collapsedHeaderViewHeight)) {
headerViewHeightDestinationConstant = collapsedHeaderViewHeight
} else {
headerViewHeightDestinationConstant = expandedHeaderViewHeight
}
if (headerViewHeight != expandedHeaderViewHeight && headerViewHeight != collapsedHeaderViewHeight) {
let animationDuration = 0.25
UIView.animate(withDuration: animationDuration, animations: { () -> Void in
self.headerViewHeight = headerViewHeightDestinationConstant
let progress = (self.headerViewHeight - self.collapsedHeaderViewHeight) / (self.expandedHeaderViewHeight - self.collapsedHeaderViewHeight)
self.headerView.expandToProgress(progress)
self.view.layoutIfNeeded()
})
}
}
}
extension APDynamicHeaderTableViewController : UICollectionViewDelegate {
}
extension APDynamicHeaderTableViewController : UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// Clamp the beginning point to 0 and the max content offset to prevent unintentional resizing when dragging during rubber banding
tableViewScrollOffsetBeginDraggingY = min(max(scrollView.contentOffset.y, 0), scrollView.contentSize.height - scrollView.frame.size.height)
// Keep track of whether or not the header was collapsed to determine if we can add the delay of expansion
headerBeganCollapsed = (headerViewHeight == collapsedHeaderViewHeight)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Do nothing if the table view is not scrollable
if feedCV.contentSize.height < feedCV.bounds.height {
return
}
var contentOffsetY = feedCV.contentOffset.y - tableViewScrollOffsetBeginDraggingY
// Add a delay to expanding the header only if the user began scrolling below the allotted amount of space to actually expand the header with no delay (e.g. If it takes 30 pixels to scroll up the scrollview to expand the header then don't add the delay of the user started scrolling at 10 pixels)
if tableViewScrollOffsetBeginDraggingY > ((expandedHeaderViewHeight - collapsedHeaderViewHeight) + headerExpandDelay) && contentOffsetY < 0 && headerBeganCollapsed {
contentOffsetY = contentOffsetY + headerExpandDelay
}
// Calculate how much the header height will change so we can readjust the table view's content offset so it doesn't scroll while we change the height of the header
let changeInHeaderViewHeight = headerViewHeight - min(max(headerViewHeight - contentOffsetY, collapsedHeaderViewHeight), expandedHeaderViewHeight)
headerViewHeight = min(max(headerViewHeight - contentOffsetY, collapsedHeaderViewHeight), expandedHeaderViewHeight)
let progress = (headerViewHeight - collapsedHeaderViewHeight) / (expandedHeaderViewHeight - collapsedHeaderViewHeight)
// Logger.verbose("headerViewHeight:",headerViewHeight)
headerView.expandToProgress(progress)
headerView.snp.updateConstraints { (make) -> Void in
make.height.equalTo(headerViewHeight)
}
// When the header view height is changing, freeze the content in the table view
if headerViewHeight != collapsedHeaderViewHeight && headerViewHeight != expandedHeaderViewHeight {
feedCV.contentOffset = CGPoint(x: 0, y: feedCV.contentOffset.y - changeInHeaderViewHeight)
}
}
// Animate the header view when the user ends dragging or flicks the scroll view
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
animateHeaderViewHeight()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
animateHeaderViewHeight()
}
}
extension APDynamicHeaderTableViewController : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: VideoCollectionViewCell.ID, for: indexPath) as! VideoCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return largeWideSize
}
}
To make hidesBarsOnSwipe working properly, your view controller's view must contain only UITableView instance and nothing else.

Make UICollectionView scroll right to left?

The natural direction for a UICollectionView to scroll when set horizontally is from left to right. Is there any way to reverse this? The simpler the better.
I'm not sure exactly what you mean -- if you set the scrolling to horizontal, it scrolls equally well, left and right. If you want it to start it from the right side, you can use this method:
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.theData.count - 1 inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
This assumes that you have 1 section, and the array populating the collection view is called theData.
Swift4 solution
in cellForItemAt collectionView function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = categoryBook.dequeueReusableCell(withReuseIdentifier: "HomeCategoryCell", for: indexPath) as! HomeCategoryCell
collectionView.transform = CGAffineTransform(scaleX:-1,y: 1);
cell.transform = CGAffineTransform(scaleX:-1,y: 1);
}
but this solution in some cases did not work properly if it dose not you can use ColletctionView scrollToItem method and you can implement it after you reload the data .
self.YourCollectionView.reloadData()
self.YourCollectionView.scrollToItem(at: NSIndexPath(item: self.YourObjectListData.count - 1, section: 0) as IndexPath, at: .right, animated: false)
Same thing for swift:
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: theData.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)
Use This Extention
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true //RETURN true if collection view needs to enable RTL
}
}
I have found using xCode 12.4 with an app that targets iOS 12 that this there seems to be no need to load the items in a different order or do any transforms. The only issue has to do with the initial scroll position. So all I need to do to get things working in both RTL and LTR is the following:
collectionView.reloadData {
if self.collectionView.effectiveUserInterfaceLayoutDirection == .rightToLeft {
self.collectionView?.scrollToItem(at: IndexPath(row:0, section:0), at: .right, animated: false)
}
}