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
Resetting the contentOffset probably is the best solution figured out so far.
A few steps should be taken to achieve this:
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/
@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
}
}
@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
}
}
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);
}
}