UICollectionViewFlowLayout implementation - ios

I am trying to accomplish the following, I've done this easily with UIScrollview but I've been experimenting with UICollectionView lately (I know I'm pretty late to the game) and would love to know if in order to do what I want I have to implement a custom layout or if FlowLayout already does this for me.
Basically, if you look at the attachment, you will notice that scrolling can happen both vertically and horizontally, the rows go all the way to beyond the UICollectionView height. The same happens with columns going beyond the width of the collection view.
Is this possible to do with Flowlayout?

I've done it like this.
#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;
}
My data source was an array of arrays with each inner array providing the data for an individual row.
After Edit:
My collection view scrolled in both directions. This is what I had in my viewDidload to set things up:
- (void)viewDidLoad {
self.theData = #[#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"], #[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"0",#"1",#"2",#"3",#"4",#"5",#"6",#"7",#"8",#"9",#"10",#"11",#"12",#"13",#"14",#"15",#"16",#"17",#"18",#"19",#"20"],#[#"Z0",#"Z1",#"Z2",#"Z3",#"Z4",#"Z5",#"Z6",#"Z7",#"Z8",#"Z9",#"Z10",#"Z11",#"Z12",#"Z13",#"Z14",#"Z15",#"Z16",#"Z17",#"Z18",#"Z19",#"Z20"]];
MultpleLineLayout *layout = [[MultpleLineLayout alloc] init];
self.collectionView = [[RDCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
self.view.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.collectionView];
[self.collectionView registerClass:[DataCell class] forCellWithReuseIdentifier:#"DataCell"];
[self.collectionView reloadData];
}

Related

Cells order in UICollectionView

I need to create a UICollectionView with cells of different sizes (1x1, 2x2, 2x1, 3x2.5). I've already code to add cells depending on which size they are using collectionView:layout:sizeForItemAtIndexPath:.
Here is expected result (cells 1 and 3 have to be on the left of cell 2) :
But current result is :
My (ugly) current code is (self.cellSize = screen width / 3) :
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat size = self.cellSize;
if (indexPath.item == 0) {
return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
}
if (indexPath.item == 1) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 2) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize * 2.f);
}
if (indexPath.item == 3) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 4) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize);
}
if (indexPath.item == 5) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 6) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 7) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize);
}
if (indexPath.item == 8) {
return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
}
return CGSizeMake(size, size);
}
Is there a way to specify that cell 3 has to be above cell 1, but on left of cell 2 ?
I don't want to do a static layout that will not change because each cell could be of different size in future. For example, cell 2 could be 2x1 sized ...
Which is the best approach to do that ? I was expecting to specify to UICollectionViewLayout a flow (like "from top"), but it doesn't work like that ...
A sample example by subclassing UICollectionViewLayout.
All values are hard coded, just to explicit the logic behind it. Of course, that could be optimized.
#interface CustomCollectionViewLayout ()
#property (nonatomic, strong) NSMutableDictionary *cellLayouts;
#property (nonatomic, assign) CGSize unitSize;
#end
#implementation CustomCollectionViewLayout
-(id)initWithSize:(CGSize)size
{
self = [super init];
if (self)
{
_unitSize = CGSizeMake(size.width/3,150);
_cellLayouts = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void)prepareLayout
{
for (NSInteger i = 0; i < [[self collectionView] numberOfItemsInSection:0]; i ++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect frame;
switch ([indexPath item])
{
case 0:
frame = CGRectMake(0, 0, _unitSize.width*3, _unitSize.height*2.5);
break;
case 1:
frame = CGRectMake(0, _unitSize.height*2.5, _unitSize.width, _unitSize.height);
break;
case 2:
frame = CGRectMake(_unitSize.width, _unitSize.height*2.5, _unitSize.width*2, _unitSize.height*2);
break;
case 3:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 4:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
break;
case 5:
frame = CGRectMake(_unitSize.width*2, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 6:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 7:
frame = CGRectMake(_unitSize.width, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
break;
case 8:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*3, _unitSize.height*2.5);
break;
default:
frame = CGRectZero;
break;
}
[attributes setFrame:frame];
[[self cellLayouts] setObject:attributes forKey:indexPath];
}
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *retAttributes = [[NSMutableArray alloc] init];
for (NSIndexPath *anIndexPath in [self cellLayouts])
{
UICollectionViewLayoutAttributes *attributes = [self cellLayouts][anIndexPath];
if (CGRectIntersectsRect(rect, [attributes frame]))
{
[retAttributes addObject:attributes];
}
}
return retAttributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self cellLayouts][indexPath];
}
-(CGSize)collectionViewContentSize
{
return CGSizeMake(_unitSize.width*3, _unitSize.height*9);
}
#end
Then, you just have to call :
CustomCollectionViewLayout *layout = [[CustomCollectionViewLayout alloc] initWithSize:self.myCollectionView.bounds.frame.size];
[self.myCollectionView setCollectionViewLayout:layout];
Rendering :
Here it's the same logic as Larme suggested. But with less hardcode and which give you the possibility of setting the number of items you want without adding new case:.
I call "pattern" a set of 5 items. So first I define constant values:
#define MAX_COLUMN 3 // Max columns in the pattern
#define MAX_LINE_PER_PATTERN 3 // Max lines in the pattern
#define PATTERN_ITEM_COUNT 5 // Max items in the pattern
Then I create a custom layout with 2 properties NSMutableArray *layoutAttributes and CGFloat contentHeight in which I need to override the methods:
- (void)prepareLayout{
[super prepareLayout];
if (!self.layoutAttributes){
self.layoutAttributes = [[NSMutableArray alloc] init];
CGFloat cellWidth = self.collectionView.frame.size.width / MAX_COLUMN;
CGFloat cellHeight = cellWidth;
self.contentHeight = 0.f;
for (int item = 0 ; item < [self.collectionView numberOfItemsInSection:0] ; item ++){
CGFloat width, height = 0.f;
CGFloat xPos, yPos = 0.f;
NSInteger patternCount = (NSInteger)((CGFloat)item / (CGFloat)PATTERN_ITEM_COUNT);
NSInteger currentIndex = item % PATTERN_ITEM_COUNT;
switch (currentIndex) {
case 0:
{
xPos = 0.f;
yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
self.contentHeight += cellHeight;
break;
}
case 1:
{
xPos = cellWidth;
yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth * 2.f;
height = cellHeight * 2.f;
self.contentHeight += cellHeight;
break;
}
case 2:
{
xPos = 0.f;
yPos = cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
break;
}
case 3:
{
xPos = 0.f;
yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth * 2.f;
height = cellHeight;
self.contentHeight += cellHeight;
break;
}
case 4:
{
xPos = 2.f * cellWidth;
yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
break;
}
default:
NSLog(#"error with index");
break;
}
UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForRow:item inSection:0]];
attr.frame = CGRectMake(xPos,
yPos,
width,
height);
[self.layoutAttributes addObject:attr];
}
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *currentAttributes = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *attr in self.layoutAttributes) {
if (CGRectIntersectsRect(attr.frame, rect))
{
[currentAttributes addObject:attr];
}
}
return currentAttributes;
}
- (CGSize)collectionViewContentSize{
return CGSizeMake(self.collectionView.frame.size.width, self.contentHeight);
}
Then assign this custom layout to self.collectionView.collectionViewLayout and that's it. You can find more information and swift version here.

UICollectionViewLayout (Converting from Swift to Objective-C)

I'm currently following this tutorial (https://www.raywenderlich.com/99087/swift-expanding-cells-ios-collection-views) which creates a custom UICollectionViewLayout, the problem is that the tutorial is written in Swift but I'm trying to convert it to Objective-C for my project.
The first part of reproducing the tutorial into Objective-C was fine and i've gotten to this stage.
Though, in the 2nd part when we're suppose to create a custom UICollectionViewLayout, and upon changing in the storyboard the Layout of CollectionView to Custom and setting the custom class, a blank screen appears.
Below are the codes that I'd reproduced from the tutorial from Swift to Objective-C:
#implementation TimetableCustomLayout{
NSMutableArray *cache;
}
-(NSInteger)featuredItemIndex{
CGFloat dragOffset = 180;
return MAX(0, self.collectionView.contentOffset.y - dragOffset);
}
-(CGFloat)nextItemPercentageOffset{
CGFloat dragOffset = 180;
return (self.collectionView.contentOffset.y / dragOffset) - [self featuredItemIndex];
}
-(CGFloat)width{
return CGRectGetWidth(self.collectionView.bounds);
}
-(CGFloat)height{
return CGRectGetHeight(self.collectionView.bounds);
}
-(NSInteger)numberOfItems{
//Will be replaced with dynamic value later
return 5;
}
-(CGSize)collectionViewContentSize{
CGFloat dragOffset = 180;
return CGSizeMake([self height], [self width]);
}
-(void)prepareLayout{
[super prepareLayout];
cache = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
self.standardHeight = 100;
self.featuredHeight = 280;
CGRect frame = CGRectZero;
CGFloat y = 0;
for(int item = 0; item < 5; item ++){
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.zIndex = item;
CGFloat height = self.standardHeight;
if(indexPath.item == [self featuredItemIndex]){
CGFloat yOffset = self.standardHeight * [self nextItemPercentageOffset];
y = self.collectionView.contentOffset.y - yOffset;
height = self.featuredHeight;
}else if(indexPath.item == ([self featuredItemIndex] + 1) && indexPath.item != [self numberOfItems]){
CGFloat maxY = y + self.standardHeight;
height = self.standardHeight + MAX((self.featuredHeight - self.standardHeight) * [self nextItemPercentageOffset], 0);
y = maxY - height;
}
frame = CGRectMake(0, y, [self width], [self height]);
attributes.frame = frame;
[cache addObject:attributes];
y = CGRectGetMaxY(frame);
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
[super layoutAttributesForElementsInRect:rect];
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
NSLog(#"%#", cache);
for(UICollectionViewLayoutAttributes *attributes in cache){
if(CGRectIntersectsRect(attributes.frame, rect)){
[layoutAttributes addObject:attributes];
}
}
return layoutAttributes;
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return true;
}
#end
I'm also getting error, Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[UICollectionViewLayoutAttributes frame]: unrecognized selector.
I believe that the error is possibly due to incorrect translation from Swift to Objective-C, particularly this line,
Swift:
var cache = [UICollectionViewLayoutAttributes]()
Objective-C:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
This is my first question on StackOverflow, any feedback and help would be greatly appreciated, thanks in advance.
You are adding a reference to the UICollectionViewLayoutAttributes class to the cache and layoutAttributes array on initialization. And then, calling frame property to the class reference (notice the + on the error message that denotes a class method, where instance methods use -).
Replace this line:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
With this:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc] init];
The same applies to the cache variable initialization.
You can also use type-safe arrays in Objective-C using generics:
NSMutableArray<UICollectionViewLayoutAttributes*> *layoutAttributes = [[NSMutableArray alloc] init];
More information here.

UICollectionView: How to keep the items of a section in a single row?

I would like to have three sections inside a UICollectionView.
Each section shall display its items in the same row. If the items won't fit on the screen they should still remain in the same row and the user should scroll to right to see them all.
In a nutshell, I would like to prevent the items flow into next line. Hope it makes sense.
I have found some strange solutions as here. But these don't really work, if you have more than one section.
How can I tackle this problem? Is there a setting on Storyboard or do I have to use a custom Layout afterall?
Use multiple UICollectionViews, each with its own scrollable area.
I wrote this layout a while ago, but I think it should still work. The data source is an array of arrays, so each row is a new section. Here's the flow layout class,
#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);
long xValue = itemWidth/2 + path.row * (itemWidth + space);
long 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;
}

Dynamically resize UICollectionViewCell in non-scrolling UICollectionView

I have a small, single row horizontal layout UICollectionView at the top of the screen. It can contain up to a maximum of 6 items. The problem is that I want all 6 items visible without scrolling (this collection view is also going to be used in a Today extension which doesn't allow scrolling). What I want to do is reduce the cell-size and inter-item spacing a little bit to allow all 6 cells to fit.
Basically I'm trying to avoid this:
I've been playing with this for a while but I'm not sure how to approach it. I created a method that's fired every time an item is added or removed from the collection view, just before [self.collectionview reloadData] is called.
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
CGSize itemsize = CGSizeMake(58,58);
CGFloat itemsizeWidth = itemsize.width;
CGFloat totalWidth = sectionInsetLeft + sectionInsetRight +
(itemsizeWidth * _armedRemindersArray.count) +
(minItemSpacing * (_armedRemindersArray.count -2));
CGFloat reductionAmount = itemsizeWidth;
if (totalWidth > screenwidth) {
while (totalWidth > screenwidth) {
totalWidth = totalWidth - 1;
reductionAmount = reductionAmount - 1;
}
CGSize newCellSize = CGSizeMake(reductionAmount, reductionAmount);
flowLayout.itemSize = newCellSize;
}
else flowLayout.itemSize = itemsize;
}
This is the result.
Not exactly what I was expecting. Not only did it squash everything to the left and also added a second line, but I also seem to have a cell-reuse issue. Truthfully I would just use static-cells if it was even an option, but unfortunately it seems like it's not possible.
What should I be doing? Subclassing UICollectionViewFlowLayout? Won't that basically do the same thing I'm doing here with the built-in flow layout?
EDIT:
Kujey's answer is definitely closer to what I need. I still have a cell-reuse issue though.
Xcode provides an object designed for your need. It's called UICollectionViewFlowLayout and all you need to do is subclass it and place your cells the way you want. The function prepareForLayout is call every time the collection view needs to update the layout.
The piece of code you need is below :
#import "CustomLayout.h"
#define MainCell #"MainCell"
#interface CustomLayout ()
#property (nonatomic, strong) NSMutableDictionary *layoutInfo;
#end
#implementation CustomLayout
-(NSMutableDictionary *) layoutInfo
{
if (!_layoutInfo) {
_layoutInfo = [NSMutableDictionary dictionary];
}
return _layoutInfo;
}
-(void) prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
CGFloat itemWidth;
CGFloat itemSpacing;
CGFloat widthWithoutSpacing = [self collectionViewContentSize].width / ([self.collectionView numberOfItemsInSection:0]);
if (widthWithoutSpacing > [self collectionViewContentSize].height) {
itemWidth = [self collectionViewContentSize].height;
itemSpacing = ([self collectionViewContentSize].width - itemWidth*[self.collectionView numberOfItemsInSection:0])/
([self.collectionView numberOfItemsInSection:0]+1);
}
else {
itemWidth = widthWithoutSpacing;
itemSpacing = 0;
}
CGFloat xPosition = itemSpacing;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
for (NSInteger index = 0 ; index < [self.collectionView numberOfItemsInSection:section] ; index++) {
indexPath = [NSIndexPath indexPathForItem:index inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect currentFrame=itemAttributes.frame;
currentFrame.origin.x = xPosition;
currentFrame.size.width = itemWidth;
currentFrame.size.height = itemWidth;
itemAttributes.frame=currentFrame;
cellLayoutInfo[indexPath] = itemAttributes;
xPosition += itemWidth + itemSpacing;
}
}
self.layoutInfo[MainCell] = cellLayoutInfo;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (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;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[MainCell][indexPath];
}
-(CGSize) collectionViewContentSize
{
return self.collectionView.frame.size;
}
#end
You can also change the y origin of your cells if you need to center them vertically.
try with this code. I get the width and use _armedRemindersArray (i guess you use this array for the items).
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat width = screenwidth - ((sectionInsetLeft + sectionInsetRight) *_armedRemindersArray.count + minItemSpacing * (_armedRemindersArray.count -2));
CGSize itemsize = CGSizeMake(width,width);
flowLayout.itemSize = itemsize;
}
I don't know why you're setting the itemsize first, and then reducing it. I think you should do it the other way around:
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
// Find the appropriate item width (<= 58)
CGFloat screenwidth = self.view.frame.size.width;
CGFloat itemsizeWidth = (screenwidth - sectionInsetLeft - sectionInsetRight - (minItemSpacing * (_armedRemindersArray.count -2))) / _armedRemindersArray.count
itemsizeWidth = itemsizeWidth > 58 ? 58 : itemsizeWidth;
flowLayout.itemSize = CGSizeMake(itemsizeWidth, itemsizeWidth);
}
Does this work? If not, could you please include more of your code?

iOS - Creating a SpringBoard screen

I'm trying so create a collection view that behaves like the iOS springboard screen.
That means that I want to have a collection view, with all of my items, and to have a page controller underneath the grid (just like the springboard screen). I also want to implement the same edit mode, meaning that one my icons are in edit mode, I can drag them from one page to another, in such a way that a page doesn't have to be full.
I've tried to set a single collection view to scroll horizontally, but it doesn't act as I want. first of all, the icons are added one below the other, instead of one beside the other. secondly, I don't have the page dots this way, and the user can't put icons on different pages
I also tried to set a page controller, and put in every page the collection view. In this case, I get that while in edit mode, I can't go between screens. I guess that is because I have more that one collection view, and they don't interact with each other.
I though of another way, of using a scroll view, and try to put a collection view inside that scroll view. But it sounds to me like a very not elegant solution.
Does anyone have an idea about how to create such a scene?
Edit:
I think the answer for my problem is hiding in this question. It describes the same situation I want to have: a collection view, that is scrolled horizontally, with paging, and that the order of the icons is not flowing vertically, but also flowing horizontally. There is a partially good answer there. I say it's partially good since the answer describes a subclass of UICollectionViewFlowLayout, but in the sample project uploaded the subclass is of UICollectionViewLayout, and I think the whole point was to maintain the functions that the UICollectionViewFlowLayout has.
This question was asked a year ago, so I believe someone has an answer, or at least, I hope.
I have partial solution to my question.
I did use the answer (of Bogdan) to the linked question above. Here's the code:
layout.h:
#import <UIKit/UIKit.h>
#interface V9Layout : UICollectionViewLayout
#property (nonatomic, assign) NSInteger itemsInOneRow;
#property (nonatomic, assign) NSInteger itemsInOneCol;
#property (nonatomic, assign) CGSize itemSize;
#end
layout.m:
#import "V9Layout.h"
#interface V9Layout()
-(void)calculateLayoutProperties;
-(int)pagesInSection:(NSInteger)section;
#property (nonatomic, strong) NSMutableArray *frames;
#property (nonatomic, assign) CGFloat lineSpacing;
#property (nonatomic, assign) CGFloat interitemSpacing;
#property (nonatomic, assign) CGSize pageSize;
#end
#implementation V9Layout
#synthesize itemsInOneRow = _itemsInOneRow, itemsInOneCol = _itemsInOneCol, itemSize = _itemSize;
#synthesize lineSpacing = _lineSpacing, interitemSpacing = _interitemSpacing;
#synthesize frames = _frames;
- (id)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (void)setup
{
self.itemsInOneRow = 3;
self.itemsInOneCol = 4;
self.frames = [NSMutableArray array];
}
-(void)calculateLayoutProperties {
self.pageSize = self.collectionView.bounds.size;
// The width of all line spacings is equal to width of ONE item. The same for interitem spacing
CGFloat itemWidth = self.pageSize.width / (self.itemsInOneRow + 1); // +1 because we all spaces make width of one additional item
CGFloat itemHeight = self.pageSize.height / (self.itemsInOneCol + 1); // the same
NSLog(#"calculateLayoutProperties item width: %f, item height: %f", itemWidth, itemHeight);
self.itemSize = CGSizeMake(itemWidth, itemHeight);
// this part is only to make a global parameter, because I'm using this data to create the size of the UICollectionViewCell. I think I need to use a delegate method for the UICollectionViewCell to ask the UICollectionViewLayout for the calculated size of cell.
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
if (standardUserDefaults) {
[standardUserDefaults setObject:[NSNumber numberWithFloat:itemWidth] forKey:#"cellWidth"];
[standardUserDefaults setObject:[NSNumber numberWithFloat:itemHeight] forKey:#"cellHeight"];
[standardUserDefaults synchronize];
}
// spacing for x
self.lineSpacing = (self.pageSize.width - (self.itemsInOneRow * self.itemSize.width)) / (self.itemsInOneRow + 1);
// spacing for y
self.interitemSpacing = (self.pageSize.height - (self.itemsInOneCol * self.itemSize.height)) / (self.itemsInOneCol + 1);
}
-(int)pagesInSection:(NSInteger)section {
NSLog(#"v9layout: numberOfItemsInSection is %ld", (long)[self.collectionView numberOfItemsInSection:section]);
return ([self.collectionView numberOfItemsInSection:section] - 1) / (self.itemsInOneRow * self.itemsInOneCol) + 1;
}
-(CGSize)collectionViewContentSize {
// contentSize.width is equal to pages * pageSize.width
NSInteger sections = 1;
if ([self.collectionView respondsToSelector:#selector(numberOfSections)]) {
sections = [self.collectionView numberOfSections];
}
int pages = 0;
for (int section = 0; section < sections; section++) {
pages = pages + [self pagesInSection:section];
}
return CGSizeMake(pages * self.pageSize.width, self.pageSize.height);
}
-(void)prepareLayout {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self calculateLayoutProperties];
});
[self.frames removeAllObjects];
NSInteger sections = 1;
if ([self.collectionView respondsToSelector:#selector(numberOfSections)]) {
sections = [self.collectionView numberOfSections];
}
int pagesOffset = 0; // Pages that are used by prevoius sections
int itemsInPage = self.itemsInOneRow * self.itemsInOneCol;
for (int section = 0; section < sections; section++) {
NSMutableArray *framesInSection = [NSMutableArray array];
int pagesInSection = [self pagesInSection:section];
int itemsInSection = [self.collectionView numberOfItemsInSection:section];
for (int page = 0; page < pagesInSection; page++) {
int itemsToAddToArray = itemsInSection - framesInSection.count;
int itemsInCurrentPage = itemsInPage;
if (itemsToAddToArray < itemsInPage) { // If there are less cells than expected (typically last page of section), we go only through existing cells.
itemsInCurrentPage = itemsToAddToArray;
}
for (int itemInPage = 0; itemInPage < itemsInCurrentPage; itemInPage++) {
CGFloat originX = (pagesOffset + page) * self.pageSize.width + self.lineSpacing + (itemInPage % self.itemsInOneRow) * (self.itemSize.width + self.lineSpacing);
CGFloat originY = self.interitemSpacing + (itemInPage / self.itemsInOneRow) * (self.itemSize.height + self.interitemSpacing);
CGRect itemFrame = CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height);
[framesInSection addObject:NSStringFromCGRect(itemFrame)];
}
}
[self.frames addObject:framesInSection];
pagesOffset += pagesInSection;
}
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *attributes = [NSMutableArray array];
NSInteger sections = 1;
if ([self.collectionView respondsToSelector:#selector(numberOfSections)]) {
sections = [self.collectionView numberOfSections];
}
int pagesOffset = 0;
int itemsInPage = self.itemsInOneRow * self.itemsInOneCol;
for (int section = 0; section < sections; section++) {
int pagesInSection = [self pagesInSection:section];
int itemsInSection = [self.collectionView numberOfItemsInSection:section];
for (int page = 0; page < pagesInSection; page++) {
CGRect pageFrame = CGRectMake((pagesOffset + page) * self.pageSize.width, 0, self.pageSize.width, self.pageSize.height);
if (CGRectIntersectsRect(rect, pageFrame)) {
int startItemIndex = page * itemsInPage;
int itemsInCurrentPage = itemsInPage;
if (itemsInSection - startItemIndex < itemsInPage) {
itemsInCurrentPage = itemsInSection - startItemIndex;
}
for (int itemInPage = 0; itemInPage < itemsInCurrentPage; itemInPage++) {
UICollectionViewLayoutAttributes *itemAttributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:startItemIndex + itemInPage inSection:section]];
if (CGRectIntersectsRect(itemAttributes.frame, rect)) {
[attributes addObject:itemAttributes];
}
}
}
}
pagesOffset += pagesInSection;
}
return attributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.frame = CGRectFromString(self.frames[indexPath.section][indexPath.item]);
return attributes;
}
#end
This implement the horizontal paging with order of cells one beside each other and not one underneath each other.
I still want to add a drag and drop ability so that the user can change the order of cells. I poset this question about it.
Hopefully, I'll find answer to this and update this answer.

Resources