By Paul Peelen


2013-03-21 13:42:31 8 Comments

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.

Continuous scroll

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.

4 comments

@Jiang Wang 2018-03-24 11:52:47

Resetting the contentOffset probably is the best solution figured out so far. infinite scrolling final result

A few steps should be taken to achieve this:

  1. 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;
  2. At start, the collection view’s contentOffset is calculated to show only the original data set (drawn in black rectangles);
  3. 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.

enter image description here

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:

http://www.awsomejiang.com/2018/03/24/Infinite-Scrolling-and-the-Tiling-Logic/

@TreeBucket 2017-02-22 04:38:30

@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
    }
}

@lorenzo 2017-04-25 13:39:06

You saved my day thanks! :o)

@Abhishek Thapliyal 2017-10-27 10:15:53

@TreeBucket : Its working but getting space between sections: How i can remove that space?

@Aditya Srivastava 2017-12-04 09:36:22

What if I have to use header view inside collectionview section. The code is not working for that. I mean it's not showing headerview

@rdelmar 2013-03-21 17:15:33

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);
    }
}

@Paul Peelen 2013-03-21 18:39:40

I played with that idea as well, but when I made an example project it seemed it loaded all the view at load, is that correct? It got really slow. (however I tried with an amount of 100.000 instead of 1000). Is there some way of tricking the UICollectionView to think it is showing section and row 10, while it actually is showing 510 or so? If so, one could make it infinite.

@rdelmar 2013-03-21 19:28:39

@PaulPeelen, No, it shouldn't load all the views at once -- just like a table view, it only creates enough cells that it needs to display on screen. I'm not sure why this gets jerky if you have more than 1000 cells in each direction, but 1000 seems like enough to make it seem infinite to the user. This example project I made isn't slow to load or scroll.

@rdelmar 2013-03-23 04:31:33

@PaulPeelen, I've edited my post to show how to do it in a truly infinite way. I only need to return a modest 4 times the size of my actual array to do it this way. It seems totally seamless.

@Paul Peelen 2013-03-24 21:02:40

awesome, I had gone the same route already but hadn't finished it. I was working on my own UICollectionView. I'll try your code out tomorrow. Since this is not the first time we've been in contact, if you like.. do add me on Skype =)

@Paul Peelen 2013-03-25 15:11:11

Worked like a charm! Thanks mate. I'll write a blog post on it sometime and link you in the credit!

@Moshe 2014-01-08 20:30:38

Paul, where's the blog post?

@Alex Cio 2014-03-04 16:32:03

I tried to implement your code. I get a gridLayout but inserting the method layoutSubviews causes my view to repeat always the same. I also get sometimes the exception [UICollectionViewData validateLayoutInRect:] after scrolling a little bit. does somebody of you have some more input how to get it work?

@Renish Dadhaniya 2014-06-05 12:29:07

+1, thanks. @rdelmar can you provide your source code demo?

@rdelmar 2014-06-05 15:40:16

@RenishDadhaniya, you can find my test app here, jmp.sh/Dn5kU5W

@ViruMax 2014-07-16 12:36:50

@rdelmar how to turn off infinite scrolling in this example. Means 1 to 10 , and then end of scrolling. 1 should not come after 10.

@rdelmar 2014-07-16 14:42:37

@ViruMax, If you don't want the infinite scrolling don't use the custom subclass of UICollectionView (that has the layoutSubviews method posted above), and return the correct number of items in numberOfItemsInSection and numberOfSectionsInCollectionView (I'm returning 4 times my actual values so you can scroll a ways before re-centereing the scroll view). Also, in cellForItemAtIndexPath, you don't need the %4 or %5 after the indexPath, it would just be self.theData[indexPath.section][indexPath.row].

@ViruMax 2014-07-16 15:15:24

@rdelmar thanks for your reply, I removed the subclas RDCollectionView but I am getting following error: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView recieved layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0xa317bb0> {length = 2, path = 0 - 5}'

@rdelmar 2014-07-16 15:19:09

@ViruMax, are you returning the correct values in numberOfItemsInSection and numberOfSectionsInCollectionView appropriate for you data structure?

@ViruMax 2014-07-16 15:21:23

@rdelmar yes I have used 5*5 dimensional array and returning 5 in numberOfItemsInSection and 5 in numberOfSectionsInCollectionView, but application didn't crash for 5*4 dimensional array which is your default array.

@ViruMax 2014-07-16 15:35:15

@rdelmar I have posted my question with code stackoverflow.com/questions/24784877/…

@testing 2015-03-30 14:09:37

@rdelmar: I tried your code, but the result isn't that what I expected. See an image here. The rows and columns are repeating itself multiple times one row after another. Has this code ever worked or am I doing something wrong?

@rdelmar 2015-03-30 15:37:08

@testing, Yes this code worked. I always test the code I post in answers, so you must be doing something wrong.

@testing 2015-03-31 07:56:57

@rdelmar: Do you have used storyboard? Where do you subclassed UICollectionView? I don't see it in your code. You are using a normal UICollectionVIew class. Is DataCell a nib or created in code? I think it's the latter one. Is collectionView placed on a nib and connected via the outlet? When you add a collection view via Interface Builder he wants a reuuse identifier and the registering of DataCell is done in Interface Builder and in code which leads to a black screen.

@testing 2015-03-31 12:19:27

@rdelmar: I got it manged. It crashes if you move fast to the left: 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x7a7cdc20> {length = 2, path = 0 - 20}

@Aditya Srivastava 2017-12-04 09:35:58

What if I have to use header view inside collectionview section. The code is not working for that. I mean it's not showing headerview

@BadPirate 2015-01-04 22:13:40

@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
    }
}

@Paul Peelen 2015-01-05 00:30:53

Nice! I'll have some use for this as well, I think.

@BadPirate 2015-01-05 09:23:28

Gets a little confusing around the concept of x vs y vs row vs column vs row vs section -- Hard to keep the orientation straight all the way through :) If anyone is thinking of copy pasting this send me a note and I'll update with the latest changes.

@Abhishek Thapliyal 2017-10-28 06:09:18

@BadPirate : Its working but getting space between sections: How i can remove that space?

@Aditya Srivastava 2017-12-04 09:36:34

What if I have to use header view inside collectionview section. The code is not working for that. I mean it's not showing headerview

Related Questions

Sponsored Content

19 Answered Questions

[SOLVED] UIScrollView: paging horizontally, scrolling vertically?

48 Answered Questions

[SOLVED] Vertically align text to top within a UILabel

2 Answered Questions

[SOLVED] Horizontal Scroll View inside Vertical Scroll view doesn't work

  • 2014-05-24 11:52:19
  • Milen
  • 2088 View
  • 3 Score
  • 2 Answer
  • Tags:   ios uiscrollview

1 Answered Questions

2 Answered Questions

[SOLVED] UICollectionview scroll horizontal and vertical

2 Answered Questions

1 Answered Questions

Both Vertical and Horizontal Scroll Direction in UICollectionView

0 Answered Questions

Passing horizontal scrolling down to a UICollectionView

2 Answered Questions

[SOLVED] UICollectionView and ScrollView inside View with vertical scrolling

Sponsored Content