How to make a UICollectionView global header and/or footer? - ios

I’ve been trying to create a UICollectionView header that would stick on top of my collection view. I’m using UICollectionViewCompositionalLayout.
I’ve tried multiple approaches: using a cell, using a section header and try to mess with insets and offsets to position it correctly relative to my content… And even adding a view on top of the collection view that would listen to the collection view’s scroll view’s contentOffset to position itself at the right place. But none of these approaches are satisfying. They all feel like a hack.
I’ve been doing some research and apparently you’d have to sublcass UICollectionViewLayout which is super tedious and seems overkill to just have a header, but one that is global to the whole collection view.

TL;DR
UICollectionViewCompositionalLayout has a configuration property which you can set by creating an UICollectionViewCompositionalLayoutConfiguration object. This object has some really nice and useful functionality such as the boundarySupplementaryItems property.
From the docs:
An array of the supplementary items that are associated with the boundary edges of the entire layout, such as global headers and footers.
Bingo. Set this property and do the necessary wiring in your datasource and you should have your global header.
Code Example
Here, I'm declaring a global header in my layout. The header is a segmented control inside a visual effect view, but yours can be any subclass of UICollectionReusableView.
enum SectionLayoutKind: Int, CaseIterable {
case description
}
private var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
}
static func descriptionSection() -> NSCollectionLayoutSection {
// Instantiate and return a `NSCollectionLayoutSection` object.
}
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
// Create your section
// add supplementaries such as header and footers that are relative to the section…
guard let layoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let section: NSCollectionLayoutSection
switch layoutKind {
case .description:
section = Self.descriptionSection()
}
return section
}
/*
✨ Magic starts HERE:
*/
let globalHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(44))
Constants.HeaderKind.globalSegmentedControl, alignment: .top)
let globalHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: globalHeaderSize, elementKind: Constants.HeaderKind.space, alignment: .top)
// Set true or false depending on the desired behavior
globalHeader.pinToVisibleBounds = true
let config = UICollectionViewCompositionalLayoutConfiguration()
/*
If you want to do spacing between sections.
That's another big thing this config object does.
If you try to define section spacing at the section level with insets,
the spacing is between the items and the standard headers.
*/
config.interSectionSpacing = 20
config.boundarySupplementaryItems = [globalHeader]
layout.configuration = config
/*
End of magic. ✨
*/
return layout
}
struct Constants {
struct HeaderKind {
static let space = "SpaceCollectionReusableView"
static let globalSegmentedControl = "segmentedControlHeader"
}
}
Supplementary code for the data source part:
let globalHeaderRegistration = UICollectionView.SupplementaryRegistration<SegmentedControlReusableView>(elementKind: Constants.HeaderKind.globalSegmentedControl) { (header, elementKind, indexPath) in
// Opportunity to further configure the header
header.segmentedControl.addTarget(self, action: #selector(self.onSegmentedControlValueChanged(_:)), for: .valueChanged)
}
dataSource.supplementaryViewProvider = { (view, kind, indexPath) in
if kind == Constants.HeaderKind.globalSegmentedControl {
return self.collectionView.dequeueConfiguredReusableSupplementary(using: globalHeaderRegistration, for: indexPath)
} else {
// return another registration object
}
}

Related

Collectionview inside collectionview cell causes flickering while scroll

I have collectionview with multiple section and vertical scrolling.
Each have only one item.
Screen flow is like below:
I call an api which provides me feedId and url for to fetch section data.
When user scrolls collectionview api gets called for section whichever section is currently visible on screen.
Inside each collectionview cell (or section) there is another collectionview with grid layout which renders grid image like below
Outer Collectionview
I am using Diffable datasource with compositional layout and while applying the snapshot the code is like below.
if #available(iOS 15.0, *) {
updateSnapshot.reconfigureItems([item])
} else {
updateSnapshot.reloadItems([item])
// Fallback on earlier versions
}
if #available(iOS 15.0, *) {
dataSource.applySnapshotUsingReloadData(updateSnapshot)
} else {
dataSource.apply(updateSnapshot, animatingDifferences: false, completion: nil)
// Fallback on earlier versions
}
Inner collectionview code is like below
var dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, GlobalGalleryViewModel>> {[] (ds, cv, indexPath, vm) -> UICollectionViewCell in
let cell = cv.dequeueReusableCell(withReuseIdentifier: GlobalGalleryCell.className, for: indexPath) as! GlobalGalleryCell
cell.configureCell(vm)
return cell
}
Issue
When I scroll the outer collectionview there is lots of glitches and flickering when inner gallery renders on outer collectionview cell.
Layout code for Outer collectionview
let parentItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension:feedInterface.isHaveActualFeed ? .estimated(height) : .fractionalHeight(1)
)
let largeItem = NSCollectionLayoutItem(layoutSize: parentItemSize)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: parentInset, leading: parentInset, bottom: parentInset, trailing: parentInset)
// Outer Group
let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension:.estimated(height))
let outerGroup = NSCollectionLayoutGroup.vertical(layoutSize: outerGroupSize, subitems: [largeItem])
Also added the cell prefetching like this
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
self.updateVisibleCellRemotely(indexPaths)
if let lastIndexPath = indexPaths.last,
lastIndexPath.section == (self.feeds.count - 1),
self.feeds.count > 0 {
self.viewModel.fetchBottomFeedData()
}///load more vertical or main feed or parent feed
}
Any solution will be appreciated.

How can I detect orthogonal scroll events when using `UICollectionViewCompositionalLayout`?

In the video Advances in Collection View Layout - WWDC 2019, Apple introduces a new 'orthogonal scrolling behavior' feature. I have a view controller almost identical to OrthogonalScrollingViewController in their example code. In particular my collection view is laid out vertically, and each section can scroll horizontally (I use section.orthogonalScrollingBehavior = .groupPaging).
I want to have all my sections scroll horizontally in unison. Previously, I listened for scrollViewDidScroll on each horizontal collection view, then manually set the content offset of the others. However, with the new orthogonalScrollingBehavior implementation, scrollViewDidScroll never gets called on the delegate when I scroll horizontally. How can I detect horizontal scrolling events with the new API?
If there's another way to make the sections scroll together horizontally, I'm also open to other suggestions.
You can use this callback:
let section = NSCollectionLayoutSection(group: group)
section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in
}
As mentioned you can use visibleItemsInvalidationHandler which provides the location of the scroll offset.
You can detect if a page changed by getting the modulus of the page width. You need to additionally supply a tolerance to ignore halfway scroll changes.
Im using this:
class CollectionView: UICollectionViewController {
private var currentPage: Int = 0 {
didSet {
if oldValue != currentPage {
print("The page changed to \(currentPage)")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Configure layout...
let itemSize = NSCollectionLayoutSize...
let item = NSCollectionLayoutItem...
let groupSize = NSCollectionLayoutSize...
let group = NSCollectionLayoutGroup.horizontal...
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
// Use visibleItemsInvalidationHandler to make calculations
section.visibleItemsInvalidationHandler = { [weak self] items, location, environment in
guard let self = self else { return }
let width = self.collectionView.bounds.width
let scrollOffset = location.x
let modulo = scrollOffset.truncatingRemainder(dividingBy: width)
let tolerance = width/5
if modulo < tolerance {
self.currentPage = Int(scrollOffset/width)
}
}
self.collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(section: section)
}
}
Here's a hacky solution. Once you render your orthogonal section, you can access it via the subviews on your collectionView. You can then check if the subview is subclass of UIScrollView and replace the delegate.
collectionView.subviews.forEach { (subview) in
if let v = subview as? UIScrollView {
customDelegate.originalDelegate = v.delegate!
v.delegate = customDelegate
}
}
One tricky bit is that you want to capture its original delegate. The reason for this is because I notice that you must call originalDelegate.scrollViewDidScroll(scrollView) otherwise the section doesn't render out completely.
In other word something like:
class CustomDelegate: NSObject, UIScrollViewDelegate {
var originalDelegate: UIScrollViewDelegate!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
originalDelegate.scrollViewDidScroll?(scrollView)
}
}
You can do this:
section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
let indexPath = visibleItems.last!.indexPath
self?.pageControl.currentPage = indexPath.row
}
The collectionView delegate willDisplay method will tell you when a cell is added to the collectionView (e.g. is displayed on screen, as they are removed when they go offscreen).
That should let you know that panning has effectively occurred (and in most cases the important part is not the pan gesture or animation but how it affects the displayed content).
In that delegate method, collectionView.visibleCells can be used to determine what cells are displayed and from that one can derive the position.
I have found one convenient way to handle this issue, you can avoid setting orthogonal scrolling and use configuration instead this way:
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(sectionProvider:sectionProvider,configuration: config)
This will call all scroll delegates for collectionview. Hope this will be helpful for someone.

UIScrollView delegate methods not called when UICollectionViewCompositionalLayout is set

I currently have a UICollectionView using UICollectionViewCompositionalLayout. I would like to animate some views within the current visible cells while scrolling / scrolling stops.
Unfortunately it seems setting orthogonalScrollingBehavior on a section to anything but .none hijacks the UICollectionView accompanying UIScrollView delegate methods.
Was wondering if there're any current workaround for this? To get the paging behaviour and UIScrollView delegate?
Setup layout
enum Section {
case main
}
override func awakeFromNib() {
super.awakeFromNib()
collectionView.collectionViewLayout = createLayout()
collectionView.delegate = self
}
func configure() {
snapshot.appendSections([.main])
snapshot.appendItems(Array(0..<10))
dataSource.apply(snapshot, animatingDifferences: false)
}
private func createLayout() -> UICollectionViewLayout {
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
)
leadingItem.contentInsets = .zero
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
),
subitems: [leadingItem])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = .groupPaging // WOULD LIKE PAGING & UISCROLLVIEW TO ALSO BE FIRED
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)
return layout
}
UICollectionViewDelegate
extension SlidingCardView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// THIS IS FIRED BUT UISCROLLVIEW METHODS NOT
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
print(111)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
print("1111111")
}
}
Setting orthogonalScrollingBehavior to a section, embeds an internal _UICollectionViewOrthogonalScrollerEmbeddedScrollView which handles the scrolling in a section. This internal scrollview is added as a subview to your collection view.
When you set yourself as a delegate to your collection view you should receive the scroll view delegate callbacks BUT ONLY for the main collection view, that scrolls between the sections and not the items in a section. Since the internal scrollviews (which may also be collectionViews, not sure) are completely different instances and you are not setting yourself as a delegate to them, you are not receiving their callbacks.
So as far as i know, there should not be an official way to receive these callbacks from the internal scrollviews that handle the scrolling in sections.
but if you are curious and you want to experiment with that you could use this 'hacked' collectionView class:
import UIKit
final class OrtogonalScrollingCollectionView: UICollectionView {
override var delegate: UICollectionViewDelegate? {
get { super.delegate }
set {
super.delegate = newValue
subviews.forEach { (view) in
guard String(describing: type(of: view)) == "_UICollectionViewOrthogonalScrollerEmbeddedScrollView" else { return }
guard let scrollView = view as? UIScrollView else { return }
scrollView.delegate = newValue
}
}
}
}
that would set your delegate to all internal scrollview that come with the orthogonal sections. You should not be using this in production environment, because there is no guarantee that Apple will keep the inner workings of the collection views the same way so this hack may not work in the future, plus you might get rejected for using private APIs in UIKit when you submit a build for release.
You may just want to use visibleItemsInvalidationHandler callback of your NSCollectionLayoutSection it acts like the UIScrollViewDelegate it will be invoked each time the section scrolls
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.visibleItemsInvalidationHandler = { (visibleItems, point, env) -> Void in
print(point)
}
Following #Stoyan answer, I fine tuned the class to be compatible with producition code by not looking for private APIs. Simply looking at all UIScrollView subclasses.
Also I think it's better to update the delegates during collection reload as you might not have the full view hierarchy yet when setting the delegate.
Finally, the class now recursively looks for UIScrollView so nothing is ever missed.
final class OrthogonalScrollingCollectionView: UICollectionView {
override func reloadData() {
super.reloadData()
scrollViews(in: self).forEach { scrollView in
scrollView.delegate = delegate
}
}
override func reloadSections(_ sections: IndexSet) {
super.reloadSections(sections)
scrollViews(in: self).forEach { scrollView in
scrollView.delegate = delegate
}
}
fileprivate func scrollViews(in subview: UIView) -> [UIScrollView] {
var scrollViews: [UIScrollView] = []
subview.subviews.forEach { view in
if let scrollView = view as? UIScrollView {
scrollViews.append(scrollView)
} else {
scrollViews.append(contentsOf: self.scrollViews(in: view))
}
}
return scrollViews
}
}
Here is a solution for determining which cell is in the center of the screen:
section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
guard let self = self else { return }
for visibleCell in self.collectionView.visibleCells {
let collectionViewCenterPoint = self.collectionView.center
if let relativePoint = visibleCell.superview?.convert(collectionViewCenterPoint, from: nil),
visibleCell.frame.contains(relativePoint)
{
// visibleCell is in the center of the view.
} else {
// visibleCell is outside the center of the view.
}
}
}

UI CollectionView in UICollectionView Cell Programmatically

Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.

Populating UICollectionView in reverse order

I would like to populate UICollectionView in reverse order so that the last item of the UICollectionView fills first and then the second last and so on. Actually I'm applying animation and items are showing up one by one. Therefore, I want the last item to show up first.
Swift 4.2
I found a simple solution and worked for me to show last item first of a collection view:
Inside viewDidLoad() method:
collectionView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
and inside collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) method before returning the cell:
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
(optional) Below lines will be necessary to auto scroll and show new item with smooth scroll.
Add below lines after loading new data:
if self.dataCollection.count > 0 {
self.collectionView.scrollToItem(at: //scroll collection view to indexpath
NSIndexPath.init(row:(self.collectionView?.numberOfItems(inSection: 0))!-1, //get last item of self collectionview (number of items -1)
section: 0) as IndexPath //scroll to bottom of current section
, at: UICollectionView.ScrollPosition.bottom, //right, left, top, bottom, centeredHorizontally, centeredVertically
animated: true)
}
I'm surprised that Apple scares people away from writing their own UICollectionViewLayout in the documentation. It's really very straightforward. Here's an implementation that I just used in an app that will do exactly what are asking. New items appear at the bottom, and the while there is not enough content to fill up the screen the the items are bottom justified, like you see in message apps. In other words item zero in your data source is the lowest item in the stack.
This code assumes that you have multiple sections, each with items of a fixed height and no spaces between items, and the full width of the collection view. If your layout is more complicated, such as different spacing between sections and items, or variable height items, Apple's intention is that you use the prepare() callback to do the heavy lifting and cache size information for later use.
This code uses Swift 3.0.
//
// Created by John Lyon-Smith on 1/7/17.
// Copyright © 2017 John Lyon-Smith. All rights reserved.
//
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100.00 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}
Just click on UICollectionView in storyboard,
in inspector menu under view section change semantic to Force Right-to-Left
I have attach an image to show how to do it in the inspector menu:
I'm assuming you are using UICollectionViewFlawLayout, and this doesn't have logic to do that, it only works in a TOP-LEFT BOTTOM-RIGHT order. To do that you have to build your own layout, which you can do creating a new object that inherits from UICollectionViewLayout.
It seems like a lot of work but is not really that much, you have to implement 4 methods, and since your layout is just bottom-up should be easy to know the frames of each cell.
Check the apple tutorial here: https://developer.apple.com/library/prerelease/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
The data collection does not actually have to be modified but that will produce the expected result. Since you control the following method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Simply return cells created from inverting the requested index. The index path is the cell's index in the collection, not necessarily the index in the source data set. I used this for a reversed display from a CoreData set.
let desiredIndex = dataProfile!.itemEntries!.count - indexPath[1] - 1;
Don't know if this still would be useful but I guess it might be quite useful for others.
If your collection view's cells are of the same height there is actually a much less complicated solution for your problem than building a custom UICollectionViewLayout.
Firstly, just make an outlet of your collection view's top constraint and add this code to the view controller:
-(void)viewWillAppear:(BOOL)animated {
[self.view layoutIfNeeded]; //for letting the compiler know the actual height and width of your collection view before we start to operate with it
if (self.collectionView.frame.size.height > self.collectionView.collectionViewLayout.collectionViewContentSize.height) {
self.collectionViewTopConstraint.constant = self.collectionView.frame.size.height - self.collectionView.collectionViewLayout.collectionViewContentSize.height;
}
So basically you calculate the difference between collection view's height and its content only if the view's height is bigger. Then you adjust it to the constraint's constant. Pretty simple. But if you need to implement cell resizing as well, this code won't be enough. But I guess this approach may be quite useful. Hope this helps.
A simple working solution is here!
// Change the collection view layer transform.
collectionView.transform3D = CATransform3DMakeScale(1, -1, 1)
// Change the cell layer transform.
cell.transform3D = CATransform3DMakeScale(1, -1, 1)
It is as simple as:
yourCollectionView.inverted = true
PS : Same for Texture/IGListKit..

Resources