View with continuous scroll; both horizontal and vertical - ios

I have been struggling with this assignment for quite some time now. What I would like to develop is a scrollview or collectionview which scrolls continuously both vertical and horizontal.
Here is an image of how I think this should look like. The transparent boxes are the views/cells which are re-loaded from the memory. As soon as a view/cell gets outside of the screen, it should be reused for upcoming new cell.. just like how a UITableViewController works.
I know that a UICollectionView can only be made to infinite scroll horizontal OR vertical, not both. However, I don't know how to do this using a UIScrollView.
I tried the code attached to an answer on this question and I can get it to re-create views (e.g. % 20) but that's not really what I need.. besides, its not continuous.
I know it is possible, because the HBO Go app does this.. I want exactly the same functionality.
My Question: How can I achieve my goal? Are there any guides/tutorials that can show me how? I can't find any.

You can get infinite scrolling, by using the technique of re-centering the UIScrollView after you get a certain distance away from the center. First, you need to make the contentSize big enough that you can scroll a bit, so I return 4 times the number of items in my sections and 4 times the number of sections, and use the mod operator in the cellForItemAtIndexPath method to get the right index into my array. You then have to override layoutSubviews in a subclass of UICollectionView to do the re-centering (this is demonstrated in the WWDC 2011 video, "Advanced Scroll View Techniques"). Here is the controller class that has the collection view (set up in IB) as a subview:
#import "ViewController.h"
#import "MultpleLineLayout.h"
#import "DataCell.h"
#interface ViewController ()
#property (weak,nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong,nonatomic) NSArray *theData;
#end
#implementation ViewController
- (void)viewDidLoad {
self.theData = #[#[#"1",#"2",#"3",#"4",#"5"], #[#"6",#"7",#"8",#"9",#"10"],#[#"11",#"12",#"13",#"14",#"15"],#[#"16",#"17",#"18",#"19",#"20"]];
MultpleLineLayout *layout = [[MultpleLineLayout alloc] init];
self.collectionView.collectionViewLayout = layout;
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.view.backgroundColor = [UIColor blackColor];
[self.collectionView registerClass:[DataCell class] forCellWithReuseIdentifier:#"DataCell"];
[self.collectionView reloadData];
}
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
return 20;
}
- (NSInteger)numberOfSectionsInCollectionView: (UICollectionView *)collectionView {
return 16;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
DataCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"DataCell" forIndexPath:indexPath];
cell.label.text = self.theData[indexPath.section %4][indexPath.row %5];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// UICollectionViewCell *item = [collectionView cellForItemAtIndexPath:indexPath];
NSLog(#"%#",indexPath);
}
Here is the UICollectionViewFlowLayout subclass:
#define space 5
#import "MultpleLineLayout.h"
#implementation MultpleLineLayout { // a subclass of UICollectionViewFlowLayout
NSInteger itemWidth;
NSInteger itemHeight;
}
-(id)init {
if (self = [super init]) {
itemWidth = 60;
itemHeight = 60;
}
return self;
}
-(CGSize)collectionViewContentSize {
NSInteger xSize = [self.collectionView numberOfItemsInSection:0] * (itemWidth + space); // "space" is for spacing between cells.
NSInteger ySize = [self.collectionView numberOfSections] * (itemHeight + space);
return CGSizeMake(xSize, ySize);
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
attributes.size = CGSizeMake(itemWidth,itemHeight);
int xValue = itemWidth/2 + path.row * (itemWidth + space);
int yValue = itemHeight + path.section * (itemHeight + space);
attributes.center = CGPointMake(xValue, yValue);
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSInteger minRow = (rect.origin.x > 0)? rect.origin.x/(itemWidth + space) : 0; // need to check because bounce gives negative values for x.
NSInteger maxRow = rect.size.width/(itemWidth + space) + minRow;
NSMutableArray* attributes = [NSMutableArray array];
for(NSInteger i=0 ; i < self.collectionView.numberOfSections; i++) {
for (NSInteger j=minRow ; j < maxRow; j++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
return attributes;
}
And finally, here is the subclass of UICollectionView:
-(void)layoutSubviews {
[super layoutSubviews];
CGPoint currentOffset = self.contentOffset;
CGFloat contentWidth = self.contentSize.width;
CGFloat contentHeight = self.contentSize.height;
CGFloat centerOffsetX = (contentWidth - self.bounds.size.width)/ 2.0;
CGFloat centerOffsetY = (contentHeight - self.bounds.size.height)/ 2.0;
CGFloat distanceFromCenterX = fabsf(currentOffset.x - centerOffsetX);
CGFloat distanceFromCenterY = fabsf(currentOffset.y - centerOffsetY);
if (distanceFromCenterX > contentWidth/4.0) { // this number of 4.0 is arbitrary
self.contentOffset = CGPointMake(centerOffsetX, currentOffset.y);
}
if (distanceFromCenterY > contentHeight/4.0) {
self.contentOffset = CGPointMake(currentOffset.x, centerOffsetY);
}
}

#updated for swift 3 and changed how the maxRow is calculated otherwise the last column is cutoff and can cause errors
import UIKit
class NodeMap : UICollectionViewController {
var rows = 10
var cols = 10
override func viewDidLoad(){
self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 400.0, itemHeight: 300.0, space: 5.0)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return rows
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return cols
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "node", for: indexPath)
}
}
class NodeLayout : UICollectionViewFlowLayout {
var itemWidth : CGFloat
var itemHeight : CGFloat
var space : CGFloat
var columns: Int{
return self.collectionView!.numberOfItems(inSection: 0)
}
var rows: Int{
return self.collectionView!.numberOfSections
}
init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
self.itemWidth = itemWidth
self.itemHeight = itemHeight
self.space = space
super.init()
}
required init(coder aDecoder: NSCoder) {
self.itemWidth = 50
self.itemHeight = 50
self.space = 3
super.init()
}
override var collectionViewContentSize: CGSize{
let w : CGFloat = CGFloat(columns) * (itemWidth + space)
let h : CGFloat = CGFloat(rows) * (itemHeight + space)
return CGSize(width: w, height: h)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
attributes.frame = CGRect(x: x, y: y, width: itemWidth, height: itemHeight)
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
let maxRow : Int = min(columns - 1, Int(ceil(rect.size.width / (itemWidth + space)) + CGFloat(minRow)))
var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
for i in 0 ..< rows {
for j in minRow ... maxRow {
attributes.append(self.layoutAttributesForItem(at: IndexPath(item: j, section: i))!)
}
}
return attributes
}
}

#rdelmar's answer worked like a charm, but I needed to do it in swift. Here's the conversion :)
class NodeMap : UICollectionViewController {
#IBOutlet var activateNodeButton : UIBarButtonItem?
var rows = 10
var cols = 10
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return rows
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return cols
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("node", forIndexPath: indexPath)
}
override func viewDidLoad() {
self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 100.0, itemHeight: 100.0, space: 5.0)
}
}
class NodeLayout : UICollectionViewFlowLayout {
var itemWidth : CGFloat
var itemHeight : CGFloat
var space : CGFloat
init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
self.itemWidth = itemWidth
self.itemHeight = itemHeight
self.space = space
super.init()
}
required init(coder aDecoder: NSCoder) {
self.itemWidth = 50
self.itemHeight = 50
self.space = 3
super.init()
}
override func collectionViewContentSize() -> CGSize {
let w : CGFloat = CGFloat(self.collectionView!.numberOfItemsInSection(0)) * (itemWidth + space)
let h : CGFloat = CGFloat(self.collectionView!.numberOfSections()) * (itemHeight + space)
return CGSizeMake(w, h)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
attributes.frame = CGRectMake(x, y, itemWidth, itemHeight)
return attributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
let maxRow : Int = Int(floor(rect.size.width/(itemWidth + space)) + CGFloat(minRow))
var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
for i in 0...self.collectionView!.numberOfSections()-1 {
for j in minRow...maxRow {
attributes.append(self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: j, inSection: i)))
}
}
return attributes
}
}

Resetting the contentOffset probably is the best solution figured out so far.
A few steps should be taken to achieve this:
Pad extra items at both the left and right side of the original data set to achieve larger scrollable area; This is similar to having a large duplicated data set, but difference is the amount;
At start, the collection view’s contentOffset is calculated to show only the original data set (drawn in black rectangles);
When the user scrolls right and contentOffset hits the trigger value, we reset contentOffset to show same visual results; but actually different data; When the user scrolls left, the same logic is used.
So, the heavy lifting is in calculating how many items should be padded both on the left and right side. If you take a look at the illustration, you will find that a minimum of one extra screen of items should be padded on left and also, another extra screen on the right. The exact amount padded depends on how many items are in the original data set and how large your item size is.
I wrote a post on this solution:
https://github.com/Alex1989Wang/Blogs/blob/master/contents/2018-03-24-Infinite-Scrolling-and-the-Tiling-Logic.md

Related

UICollectionView not rendering all rows

I'm trying to create a simple app, where one can enter a number of columns and a number of rows for an UICollectionView. The collection view then calculates the size of possible squares that fit into it and draws them.
I want to allow a maximum of 32 in width and 64 in height. Scrolling is disabled as the whole grid should be shown at once.
For example, 4x8 looks like this
and 8x4 will look like this
So as one can see that works fine. The problems comes with a higher amount of columns and/or rows. Up to 30x8 everything is fine but starting with 31 only 6 of the 8 rows are drawn.
So I don't understand why. Following is the code I use to calculate everything:
Number of section and number of rows:
func numberOfSections(in collectionView: UICollectionView) -> Int
{
let num = Int(heightInput.text!)
if(num != nil)
{
if(num! > 64)
{
return 64
}
return num!
}
return 8
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
let num = Int(widthInput.text!)
if(num != nil)
{
if(num! > 32)
{
return 32
}
return num!
}
return 4
}
Cell for item at indexPath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let size = calculateCellSize()
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
var origin = cell.frame.origin
origin.x = 1+CGFloat(indexPath.row) + size.width*CGFloat(indexPath.row)
origin.y = 1+CGFloat(indexPath.section) + size.height*CGFloat(indexPath.section)
cell.frame = CGRect(origin: origin, size: size)
NSLog("Cell X:%#, Cell Y:%#",origin.x.description,origin.y.description)
return cell
}
The calculate size method
func calculateCellSize() -> CGSize
{
//First check if we have valid values
let col = Int(widthInput.text!)
let row = Int(heightInput.text!)
if(col == nil || row == nil)
{
return CGSize(width: 48.0, height: 48.0)
}
//If there are more or equal amount of columns than rows
let columns = CGFloat(col!)
let rows = CGFloat(row!)
if(columns >= rows)
{
//Take the grid width
let gridWidth = drawCollection.bounds.size.width
//Calculate the width of the "pixels" that fit the width of the grid
var pixelWidth = gridWidth/columns
//Remember to substract the inset from the width
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelWidth -= (drawLayout?.sectionInset.left)! + 1/columns
return CGSize(width: pixelWidth, height: pixelWidth)
}
else
{
//Rows are more than columns
//Take the grid height as reference here
let gridHeight = drawCollection.bounds.size.height
//Calculate the height of the "pixels" that fit the height of the grid
var pixelHeight = gridHeight/rows
//Remember to substract the inset from the height
let drawLayout = drawCollection.collectionViewLayout as? UICollectionViewFlowLayout
pixelHeight -= (drawLayout?.sectionInset.top)! + 1/rows
return CGSize(width: pixelHeight, height: pixelHeight)
}
return CGSize(width: 48.0, height: 48.0)
}
For debugging reasons I put a counter into the cellforItemAtIndexPath method and in fact I can see that the last two rows are not called. The counter ends at 185 but in theory it should have been called 248 times and in fact the difference will show it is 2*32 - 1(for the uneven 31) so the last missing rows....
Several things came to my mind what the reason is but nothing of it seems to be:
the cells are not drawn at the right location (aka outside the grid) -> At least not correct as the method is only called 185 times.
The cells are calculated to be outside the grid therefore not tried to be rendered by the UICollectionView -> Still possible as I couldn't figure how to proof that.
There is a (if so hopefully configurable) maximum amount of elements the UICollectionView can draw and 31x8 already exceeds that number -> Still possible couldn't find anything about that.
So summary:
Is it possible to display all elements in the grid (32x64 max) and if so, what is wrong in my implementation?
Thank you all for your time and answers!
You're doing a whole lot of calculating that you don't need to do. Also, setting the .frame of a cell is a really bad idea. One big point of a collection view is to avoid having to set frames.
Take a look at this:
class GridCollectionViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet weak var theCV: UICollectionView!
var numCols = 0
var numRows = 0
func updateCV() -> Void {
// subtract the number of colums (for the 1-pt spacing between cells), and divide by number of columns
let w = (theCV.frame.size.width - CGFloat((numCols - 1))) / CGFloat(numCols)
// subtract the number of rows (for the 1-pt spacing between rows), and divide by number of rows
let h = (theCV.frame.size.height - CGFloat((numRows - 1))) / CGFloat(numRows)
// get the smaller of the two values
let wh = min(w, h)
// set the cell size
if let layout = theCV.collectionViewLayout as? UICollectionViewFlowLayout {
layout.itemSize = CGSize(width: wh, height: wh)
}
// reload the collection view
theCV.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start with a 31x20 grid, just to see it
numCols = 31
numRows = 20
updateCV()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return numRows
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numCols
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = theCV.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
#IBAction func btnTap(_ sender: Any) {
// example of changing the number of rows/columns based on user action
numCols = 32
numRows = 64
updateCV()
}
}
You need to develop your own UICollectionViewLayout. With this approach you can achieve any result human being can imagine.
import UIKit
class CollectionLayout: UICollectionViewFlowLayout {
private var width: Int = 1
private var height: Int = 1
func set(width: Int, height: Int) {
guard width > 0, height > 0 else { return }
self.height = height
self.width = width
calculateItemSize()
}
private func calculateItemSize() {
guard let collectionView = collectionView else { return }
let size = collectionView.frame.size
var itemWidth = size.width / CGFloat(width)
// spacing is needed only if there're more than 2 items in a row
if width > 1 {
itemWidth -= minimumInteritemSpacing
}
var itemHeight = size.height / CGFloat(height)
if height > 1 {
itemHeight -= minimumLineSpacing
}
let edgeLength = min(itemWidth, itemHeight)
itemSize = CGSize(width: edgeLength, height: edgeLength)
}
// calculate origin for every item
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath)
// calculate item position in the grid
let col = CGFloat(indexPath.row % width)
let row = CGFloat(Int(indexPath.row / width))
// don't forget to take into account 'minimumInteritemSpacing' and 'minimumLineSpacing'
let x = col * itemSize.width + col * minimumInteritemSpacing
let y = row * itemSize.height + row * minimumLineSpacing
// set new origin
attributes?.frame.origin = CGPoint(x: x, y: y)
return attributes
}
// accumulate all attributes
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes = [UICollectionViewLayoutAttributes]()
for attribute in attributes {
if let newAttribute = layoutAttributesForItem(at: attribute.indexPath) {
newAttributes.append(newAttribute)
}
}
return newAttributes
}
}
Set our layout to UICollectionView
Update collection view every time user enters width and height:
#IBAction func onSetTapped(_ sender: Any) {
width = Int(widthTextField.text!)
height = Int(heightTextField.text!)
if let width = width, let height = height,
let layout = collectionView.collectionViewLayout as? CollectionLayout {
layout.set(width: width, height: height)
collectionView.reloadData()
}
}

How to display 3 cell per row UICollectionView

As per the header, I'm using UICollectionView to display images. I need to display 3 cells per row, something like how Instagram does it.
Because there are many screen sizes, I'm getting the width of the iPhone screen, then divide it by 3. However, it does not show as per what I need it to be.
Here are my codes inside the viewDidLoad() method:
private let leftAndRightPaddings: CGFloat = 8.0
private let numberOfItemsPerRow: CGFloat = 3.0
private let heightAdjustment: CGFloat = 30.0
let bounds = UIScreen.mainScreen().bounds
let width = (bounds.size.width - leftAndRightPaddings) / numberOfItemsPerRow
let layout = userDetailCollection.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSizeMake(width, width)
Replace this line
let width = (bounds.size.width - leftAndRightPaddings) / numberOfItemsPerRow
with
let width = (bounds.size.width - leftAndRightPaddings*4) / numberOfItemsPerRow
As you are not considering spacing between two items & Right insets
spacing therefore it is not adjusting in the screen.
private let leftAndRightPaddings: CGFloat = 15.0
private let numberOfItemsPerRow: CGFloat = 3.0
let bounds = UIScreen.mainScreen().bounds
let width = (bounds.size.width - leftAndRightPaddings*(numberOfItemsPerRow+1)) / numberOfItemsPerRow
let layout = userDetailCollection.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSizeMake(width, width)
Try this code
overriding UICollectionViewLayout will let you much more flexibility in creating collectionViews. here is a snippet for YourOwnCollectionViewLayout : UICollectionViewLayout:
- (NSInteger)itemWidth {
return (self.collectionView.bounds.size.width - (HORIZONTAL_PAD * (NUMBER_OF_ITEMS_PER_ROW+1))) / NUMBER_OF_ITEMS_PER_ROW; // pad from each side + pad in the middle
}
- (NSInteger)itemHeight {
return self.itemWidth; // or something else
}
- (NSString *)keyForIndexPath:(NSIndexPath *)indexPath {
NSString *retVal = [NSString stringWithFormat:#"%ld-%ld", (long)indexPath.section, (long)indexPath.row];
return retVal;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat itemWidth = [self itemWidth];
CGFloat itemHeight = [self itemHeight];
CGFloat originX = HORIZONTAL_PAD + ((indexPath.row % NUMBER_OF_ITEMS_PER_ROW)*(HORIZONTAL_PAD + itemWidth));
CGFloat originY = VERTICAL_PAD + (floorf(indexPath.row/NUMBER_OF_ITEMS_PER_ROW)*(VERTICAL_PAD + itemHeight));
return CGRectMake(originX, originY, itemWidth, itemHeight);
}
- (void)prepareLayout {
NSMutableDictionary *newLayoutInfo = [NSMutableDictionary dictionary];
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
for (NSInteger section = 0; section < sectionCount; section++) {
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForItemAtIndexPath:indexPath];
NSString *key = [self keyForIndexPath:indexPath];
cellLayoutInfo[key] = itemAttributes;
}
}
newLayoutInfo[#"CellKind"] = cellLayoutInfo;
self.layoutInfo = newLayoutInfo;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
NSString *key = [self keyForIndexPath:indexPath];
UICollectionViewLayoutAttributes *retVal = self.layoutInfo[#"CellKind"][key];
return retVal;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier,
NSDictionary *elementsInfo,
BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
UICollectionViewLayoutAttributes *attributes,
BOOL *innerStop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
}];
return allAttributes;
}
- (CGSize)collectionViewContentSize {
return CGSizeMake(self.collectionView.bounds.size.width, floorf([self.collectionView numberOfItemsInSection:0]/NUMBER_OF_ITEMS_PER_ROW) * (self.itemHeight + VERTICAL_PAD) + VERTICAL_PAD);
}
in your mainView add:
[[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:[[YourOwnCollectionViewLayout alloc] init]];
and you're DONE!
You are not considering the sectionInset and the interItemSpacing. It has some default values which pushes the cells(gives padding). Either you can set the interItemSpacing to 0 and sectionInset to UIEdgeInsetsZero or you have to consider their values during calculation of the cellWidth.
Override these protocol methods for setting the size, insets and interItem spacing
optional public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
optional public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets
optional public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat
optional public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat

UICollectionViewFlowLayout Subclass causes some cells to not appear

I have a vertically scrolling UICollectionView that uses a subclass of UICollectionViewFlowLayout to try and eliminate inter-item spacing. This would result in something that looks similar to a UITableView, but I need the CollectionView for other purposes. There is a problem in my implementation of the FlowLayout subclass that causes cells to disappear when scrolling fast. Here is the code for my FlowLayout subclass:
EDIT: See Comments For Update
class ListLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var answer = super.layoutAttributesForElementsInRect(rect) {
for attr in (answer as [UICollectionViewLayoutAttributes]) {
let ip = attr.indexPath
attr.frame = self.layoutAttributesForItemAtIndexPath(ip).frame
}
return answer;
}
return nil
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAtts = super.layoutAttributesForItemAtIndexPath(indexPath) as UICollectionViewLayoutAttributes
if indexPath.item == 0 {
var frame = currentItemAtts.frame
frame.origin.y = 0
currentItemAtts.frame = frame
return currentItemAtts
}
let prevIP = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIP).frame
let prevFrameTopPoint = prevFrame.origin.y + prevFrame.size.height
var frame = currentItemAtts.frame
frame.origin.y = prevFrameTopPoint
currentItemAtts.frame = frame
return currentItemAtts
}
}
One other thing to note: My cells are variable height. Their height is set by overriding preferredLayoutAttributesFittingAttributes in the subclass of the custom cell:
override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes! {
let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as UICollectionViewLayoutAttributes
attr.frame.size = CGSizeMake(self.frame.size.width, myHeight)
return attr
}
And I set the layout's estimated size on initialization:
flowLayout.estimatedItemSize = CGSize(width: UIScreen.mainScreen().bounds.size.width, height: 60)
Here is a GIF that demonstrates this problem:
Does anybody have an idea as to what's going on? Your help is much appreciated.
Thanks!

Configure UICollectionViewFlowLayout to lay out rows from bottom to top

By default (i.e., with a vertical scrolling direction), the UICollectionViewFlowLayout lays out cells by starting at the top-left, going from left to right, until the row is filled, and then proceeds to the next line down. Instead, I would like it to start at the bottom-left, go from left to right, until the row is filled, and then proceed to the next line up.
Is there a straightforward way to do this by subclassing UIScrollViewFlowLayout, or do I basically need to re-implement that class from scratch?
Apple's documentation on subclassing flow layout suggests that I only need to override and re-implement my own version of layoutAttributesForElementsInRect:, layoutAttributesForItemAtIndexPath:, and collectionViewContentSize. But this does not seem straightforward. Since UICollectionViewFlowLayout does not expose any of the grid layout calculations it makes internally in prepareLayout, I need to deduce all the layout values needed for the bottom-to-top layout from the values it generates for a top-to-bottom layout.
I am not sure this is possible. While I can re-use its calculations about which groups of items get put on the same rows, I will need to calculate new y offsets. And to make my calculations I will need information about all the items, but those superclass methods do not report that.
The very helpful answer by #insane-36 showed a way to do it when collectionView.bounds == collectionView.collectionViewContentSize.
But if you wish to support the case where collectionView.bounds < collectionViewcontentSize, then I believe you need to re-map the rects exactly to support scrolling properly. If you wish to support the case where collectionView.bounds > collectionViewContentSize, then you need to override collectionViewContentSize to ensure the content rect is positioned at the bottom of the collectionView (since otherwise it will be positioned at the top, due to the top-to-bottom default behavior of UIScrollView).
So the full solution is a bit more involved, and I ended up developing it here: https://github.com/algal/ALGReversedFlowLayout.
You could basically implement it with a simple logic, however this seems to be some how odd. If the collectionview contentsize is same as that of the collectionview bounds or if all the cells are visible then you could implement this with simple flowLayout as this,
#implementation SimpleFlowLayout
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
[self modifyLayoutAttribute:attribute];
return attribute;
}
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
for(UICollectionViewLayoutAttributes *attribute in attributes){
[self modifyLayoutAttribute:attribute];
}
return attributes;
}
- (void)modifyLayoutAttribute:(UICollectionViewLayoutAttributes*)attribute{
CGSize contentSize = self.collectionViewContentSize;
CGRect frame = attribute.frame;
frame.origin.x = contentSize.width - attribute.frame.origin.x - attribute.frame.size.width;
frame.origin.y = contentSize.height - attribute.frame.origin.y - attribute.frame.size.height;
attribute.frame = frame;
}
#end
And so the figure looks like this,
But, if you use more rows, more than the that can be seen on the screen at the same time, then there seems to be some problem with reusing. Since the UICollectionView datasource method, collectionView:cellForItemAtIndexPath: works linearly and asks for the indexPath as the user scrolls, the cell are asked in the usual increasing indexPath pattern such as 1 --- 100 though we would want it to reverse this pattern. While scrolling we would need the collectionView to ask us for the items in decreasing order since our 100 item resides at top and 1 item at bottom. So, I dont have any particular idea about how this could be accomplished.
UICollectionView with a reversed flow layout.
import Foundation
import UIKit
class InvertedFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard super.layoutAttributesForElements(in: rect) != nil else { return nil }
var attributesArrayNew = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
if let attributeCell = layoutAttributesForItem(at: indexPathCurrent) {
if attributeCell.frame.intersects(rect) {
attributesArrayNew.append(attributeCell)
}
}
}
}
for section in 0 ..< collectionView.numberOfSections {
let indexPathCurrent = IndexPath(item: 0, section: section)
if let attributeKind = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPathCurrent) {
attributesArrayNew.append(attributeKind)
}
}
}
return attributesArrayNew
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeKind = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
attributeKind.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight - CGFloat(indexPath.section + 1) * headerHeight(indexPath.section) - sectionInset.bottom + minimumLineSpacing/2, width: collectionViewContentSize.width, height: headerHeight(indexPath.section))
}
return attributeKind
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeCell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
if section == indexPath.section && item == indexPath.item {
break
}
}
}
attributeCell.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight + minimumLineSpacing - CGFloat(indexPath.section) * headerHeight(indexPath.section) - sectionInset.bottom, width: collectionViewContentSize.width, height: cellHeight(indexPath) )
}
return attributeCell
}
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 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
height += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
height += sectionInset.bottom + CGFloat(collectionView.numberOfSections) * headerHeight(0)
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
}
func cellHeight(_ indexPath: IndexPath) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath)
return size.height
}
return 0
}
func headerHeight(_ section: Int) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, referenceSizeForHeaderInSection: section)
return size.height
}
return 0
}
}

UICollectionView: current index path for page control

I'm using a UICollectionView with a flow layout to show a list of cells, I also have a page control to indicate current page, but there seems to be no way to get current index path, I know I can get visible cells:
UICollectionView current visible cell index
however there can be more than one visible cells, even if each of my cells occupies full width of the screen, if I scroll it to have two halves of two cells, then they are both visible, so is there a way to get only one current visible cell's index?
Thanks
You can get the current index by monitoring contentOffset in scrollViewDidScroll delegate
it will be something like this
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;
}
Get page via NSIndexPath from center of view.
Works even your page not equal to width of UICollectionView.
func scrollViewDidScroll(scrollView: UIScrollView) {
let xPoint = scrollView.contentOffset.x + scrollView.frame.width / 2
let yPoint = scrollView.frame.height / 2
let center = CGPoint(x: xPoint, y: yPoint)
if let ip = collectionView.indexPathForItemAtPoint(center) {
self.pageControl.currentPage = ip.row
}
}
Definitely you need catch the visible item when the scroll movement is stopped. Use next code to do it.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let indexPath = myCollectionView.indexPathsForVisibleItems.first {
myPageControl.currentPage = indexPath.row
}
}
Swift 5.1
The easy way and more safety from nil crash
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if collectionView == newsCollectionView {
if newsPager.currentPage == indexPath.row {
guard let visible = newsCollectionView.visibleCells.first else { return }
guard let index = newsCollectionView.indexPath(for: visible)?.row else { return }
newsPager.currentPage = index
}
}
}
Place PageControl in your view or set by Code.
Set UIScrollViewDelegate
In Collectionview-> cellForItemAtIndexPath (Method) add the below
code for calculate the Number of pages,
int pages = floor(ImageCollectionView.contentSize.width/ImageCollectionView.frame.size.width);
[pageControl setNumberOfPages:pages];
Add the ScrollView Delegate method,
#pragma mark - UIScrollViewDelegate for UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = ImageCollectionView.frame.size.width;
float currentPage = ImageCollectionView.contentOffset.x / pageWidth;
if (0.0f != fmodf(currentPage, 1.0f))
{
pageControl.currentPage = currentPage + 1;
}
else
{
pageControl.currentPage = currentPage;
}
NSLog(#"finishPage: %ld", (long)pageControl.currentPage);
}
I had similar situation where my flow layout was set for UICollectionViewScrollDirectionHorizontal and I was using page control to show the current page.
I achieved it using custom flow layout.
/------------------------
Header file (.h) for custom header
------------------------/
/**
* The customViewFlowLayoutDelegate protocol defines methods that let you coordinate with
*location of cell which is centered.
*/
#protocol CustomViewFlowLayoutDelegate <UICollectionViewDelegateFlowLayout>
/** Informs delegate about location of centered cell in grid.
* Delegate should use this location 'indexPath' information to
* adjust it's conten associated with this cell.
* #param indexpath of cell in collection view which is centered.
*/
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath;
#end
#interface customViewFlowLayout : UICollectionViewFlowLayout
#property (nonatomic, weak) id<CustomViewFlowLayoutDelegate> delegate;
#end
/------------------- Implementation file (.m) for custom header -------------------/
#implementation customViewFlowLayout
- (void)prepareLayout {
[super prepareLayout];
}
static const CGFloat ACTIVE_DISTANCE = 10.0f; //Distance of given cell from center of visible rect
static const CGFloat ITEM_SIZE = 40.0f; // Width/Height of cell.
- (id)init {
if (self = [super init]) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumInteritemSpacing = 60.0f;
self.sectionInset = UIEdgeInsetsZero;
self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
self.minimumLineSpacing = 0;
}
return self;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (CGRectIntersectsRect(attribute.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attribute.center.x;
// Make sure given cell is center
if (ABS(distance) < ACTIVE_DISTANCE) {
[self.delegate collectionView:self.collectionView layout:self cellCenteredAtIndexPath:attribute.indexPath];
}
}
}
return attributes;
}
Your class containing collection view must conform to protocol 'CustomViewFlowLayoutDelegate' I described earlier in custom layout header file. Like:
#interface MyCollectionViewController () <UICollectionViewDataSource, UICollectionViewDelegate, CustomViewFlowLayoutDelegate>
#property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong, nonatomic) IBOutlet UIPageControl *pageControl;
....
....
#end
There are two ways to hook your custom layout to collection view, either in xib OR in code like say in viewDidLoad:
customViewFlowLayout *flowLayout = [[customViewFlowLayout alloc]init];
flowLayout.delegate = self;
self.collectionView.collectionViewLayout = flowLayout;
self.collectionView.pagingEnabled = YES; //Matching your situation probably?
Last thing, in MyCollectionViewController implementation file, implement delegate method of 'CustomViewFlowLayoutDelegate'.
- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath {
self.pageControl.currentPage = indexPath.row;
}
I hope this would be helpful. :)
Note - I have found andykkt's answer useful but since it is in obj-c converted it to swift and also implemented logic in another UIScrollView delegate for a smoother effect.
func updatePageNumber() {
// If not case to `Int` will give an error.
let currentPage = Int(ceil(scrollView.contentOffset.x / scrollView.frame.size.width))
pageControl.currentPage = currentPage
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// This will be call when you scrolls it manually.
updatePageNumber()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// This will be call when you scrolls it programmatically.
updatePageNumber()
}
for swift 4.2
#IBOutlet weak var mPageControl: UIPageControl!
#IBOutlet weak var mCollectionSlider: UICollectionView!
private var _currentIndex = 0
private var T1:Timer!
private var _indexPath:IndexPath = [0,0]
private func _GenerateNextPage(){
self._currentIndex = mCollectionSlider.indexPathForItem(at: CGPoint.init(x: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midX, y: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midY))?.item ?? 0
self.mPageControl.currentPage = self._currentIndex
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
_SetTimer(AutoScrollInterval)
_GenerateNextPage()
}
#objc private func _AutoScroll(){
self._indexPath = IndexPath.init(item: self._currentIndex+1, section: 0)
if !(self._indexPath.item < self.numberOfItems){
_indexPath = [0,0]
}
self.mCollectionSlider.scrollToItem(at: self._indexPath, at: .centeredHorizontally, animated: true)
}
private func _SetTimer(_ interval:TimeInterval){
if T1 == nil{
T1 = Timer.scheduledTimer(timeInterval: interval , target:self , selector: #selector(_AutoScroll), userInfo: nil, repeats: true)
}
}
you can skip the function _SetTimer() , thats for auto scroll
With UICollectionViewDelegate methods
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
pageControl.currentPage = indexPath.row
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControl.currentPage == indexPath.row {
pageControl.currentPage = collectionView.indexPath(for: collectionView.visibleCells.first!)!.row
}
}
Swift 5.0
extension youriewControllerName:UIScrollViewDelegate{
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = self.collectionView.frame.size.width
pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
}
}
(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat pageWidth = _cvImagesList.frame.size.width;
float currentPage = _cvImagesList.contentOffset.x / pageWidth;
_pageControl.currentPage = currentPage + 1;
NSLog(#"finishPage: %ld", (long)_pageControl.currentPage);
}

Resources