67

I have a UIScrollView that lays out a grid of icons. If you were to imagine the layout for the iOS Springboard, you'd be pretty close to correct. It has a horizontal, paged scroll (just like Springboard). However, it appears that the layout is not quite right. It appears as though it is laying out the items from top to bottom. As a result, my last column only has 2 rows in it, due to the number of items to be displayed. I'd rather have my last row on the last page have 2 items, like you would see in the Springboard.

How can this be accomplished with UICollectionView and its related classes? Do I have to write a custom UICollectionViewFlowLayout?

Faysal Ahmed
  • 6,464
  • 5
  • 23
  • 43
Shadowman
  • 9,072
  • 17
  • 84
  • 170

12 Answers12

99

Have you tried setting the scroll direction of your UICollectionViewFlowLayout to horizontal?

[yourFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];

And if you want it to page like springboard does, you'll need to enable paging on your collection view like so:

[yourCollectionView setPagingEnabled:YES];
Erik Hunter
  • 1,107
  • 1
  • 6
  • 9
  • 11
    Yes, but as I mentioned in the question, this fills in the collection view from top to bottom on each page. As a result, the last column is only partially filled. I would rather it fill the collection view from left to right, leaving the last ROW partially filled. – Shadowman Oct 18 '13 at 17:08
77

1st approach

What about using UIPageViewController with an array of UICollectionViewControllers? You'd have to fetch proper number of items in each UICollectionViewController, but it shouldn't be hard. You'd get exactly the same look as the Springboard has.

2nd approach

I've thought about this and in my opinion you have to set:

self.collectionView.pagingEnabled = YES;

and create your own collection view layout by subclassing UICollectionViewLayout. From the custom layout object you can access self.collectionView, so you'll know what is the size of the collection view's frame, numberOfSections and numberOfItemsInSection:. With that information you can calculate cells' frames (in prepareLayout) and collectionViewContentSize. Here're some articles about creating custom layouts:

3rd approach

You can do this (or an approximation of it) without creating the custom layout. Add UIScrollView in the blank view, set paging enabled in it. In the scroll view add the a collection view. Then add to it a width constraint, check in code how many items you have and set its constant to the correct value, e.g. (self.view.frame.size.width * numOfScreens). Here's how it looks (numbers on cells show the indexPath.row): https://www.dropbox.com/s/ss4jdbvr511azxz/collection_view.mov If you're not satisfied with the way cells are ordered, then I'm afraid you'd have to go with 1. or 2.

Community
  • 1
  • 1
Arek Holko
  • 8,793
  • 4
  • 26
  • 46
  • Adopting the 1st approach, I have problems displaying the view controller via push, when a cell is selected. How to solve that? – Scott Jul 23 '14 at 12:17
  • @Scott: What kind of problems? – Arek Holko Jul 23 '14 at 12:52
  • I've posted a [question earlier here](http://stackoverflow.com/questions/24903898/navigation-controller-lost-when-collection-view-controller-are-instantiated-unde). I'm able to load the different UICollectionViewController in each page, but have problem once I select cell, error stating _Push segues can only be used when the source controller is managed by an instance of UINavigationController_ – Scott Jul 23 '14 at 13:11
  • 1
    In case anyone wants a working example of the 2nd approach http://stackoverflow.com/questions/25963987/uicollectionview-horizontal-scroll-with-horizontal-alignment – harinsa Jul 02 '15 at 15:14
48

You need to reduce the height of UICollectionView to its cell / item height and select "Horizontal" from the "Scroll Direction" as seen in the screenshot below. Then it will scroll horizontally depending on the numberOfItems you have returned in its datasource implementation.

enter image description here

Deepak Thakur
  • 3,155
  • 2
  • 33
  • 60
18

If you are defining UICollectionViewFlowLayout in code, it will override Interface Builder configs. Hence you need to re-define the scrollDirection again.

let layout = UICollectionViewFlowLayout()
...
layout.scrollDirection = .Horizontal
self.awesomeCollectionView.collectionViewLayout = layout
DazChong
  • 3,306
  • 25
  • 23
15

This code works well in Swift 3.1 and Xcode 8.3.2

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        self.collectionView.collectionViewLayout = layout
        self.collectionView!.contentInset = UIEdgeInsets(top: -10, left: 0, bottom:0, right: 0)

        if let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.minimumInteritemSpacing = 0
            layout.minimumLineSpacing = 0
            layout.itemSize = CGSize(width: self.view.frame.size.width-40, height: self.collectionView.frame.size.height-10)
            layout.invalidateLayout()
        }

    }
Prashant Gaikwad
  • 2,229
  • 15
  • 23
  • 5
    Why don't you set those properties of the layout before you assign it to the collection view? - it would be much cleaner. – Wez Jan 23 '18 at 16:21
  • Isn't this piece of code producing an infinite loop? You call it in the `viewDidLayoutSubviews`, and then trigger the `invalidateLayout()`, which will call the `viewWillLayoutSubviews` and then `viewDidLayoutSubviews` again and again. – Starsky Feb 16 '21 at 13:53
9

Just for fun, another approach would be to just leave the paging and horizontal scrolling set, add a method that changes the order of the array items to convert from 'top to bottom, left to right' to visually 'left to right, top to bottom' and fill the in-between cells with empty hidden cells to make the spacing right. In case of 7 items in a grid of 9, this would go like this:

[1][4][7]
[2][5][ ]
[3][6][ ]

should become

[1][2][3]
[4][5][6]
[7][ ][ ]

so 1=1, 2=4, 3=7 etc. and 6=empty. You can reorder them by calculating the total number of rows and columns, then calculate the row and column number for each cell, change the row for the column and vice versa and then you have the new indexes. When the cell doesn't have a value corresponding to the image you can return an empty cell and set cell.hidden = YES; to it.

It works quite well in a soundboard app I built, so if anyone would like working code I'll add it. Only little code is required to make this trick work, it sounds harder than it is!

Update

I doubt this is the best solution, but by request here's working code:

- (void)viewDidLoad {
    // Fill an `NSArray` with items in normal order
    items = [NSMutableArray arrayWithObjects:
             [NSDictionary dictionaryWithObjectsAndKeys:@"Some label 1", @"label", @"Some value 1", @"value", nil],
             [NSDictionary dictionaryWithObjectsAndKeys:@"Some label 2", @"label", @"Some value 2", @"value", nil],
             [NSDictionary dictionaryWithObjectsAndKeys:@"Some label 3", @"label", @"Some value 3", @"value", nil],
             [NSDictionary dictionaryWithObjectsAndKeys:@"Some label 4", @"label", @"Some value 4", @"value", nil],
             [NSDictionary dictionaryWithObjectsAndKeys:@"Some label 5", @"label", @"Some value 5", @"value", nil],
              nil
              ];

    // Calculate number of rows and columns based on width and height of the `UICollectionView` and individual cells (you might have to add margins to the equation based on your setup!)
    CGFloat w = myCollectionView.frame.size.width;
    CGFloat h = myCollectionView.frame.size.height;
    rows = floor(h / cellHeight);
    columns = floor(w / cellWidth);
}

// Calculate number of sections
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return ceil((float)items.count / (float)(rows * columns));
}

// Every section has to have every cell filled, as we need to add empty cells as well to correct the spacing
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return rows*columns;
}

// And now the most important one
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier@"myIdentifier" forIndexPath:indexPath];

    // Convert rows and columns
    int row = indexPath.row % rows;
    int col = floor(indexPath.row / rows);
    // Calculate the new index in the `NSArray`
    int newIndex = ((int)indexPath.section * rows * columns) + col + row * columns;

    // If the newIndex is within the range of the items array we show the cell, if not we hide it
    if(newIndex < items.count) {
        NSDictionary *item = [items objectAtIndex:newIndex];
        cell.label.text = [item objectForKey:@"label"];
        cell.hidden = NO;
    } else {
        cell.hidden = YES;
    }

    return cell;
}

If you'd like to use the didSelectItemAtIndexPath method you have to use the same conversion that is used in cellForItemAtIndexPath to get the corresponding item. If you have cell margins you need to add them to the rows and columns calculation, as those have to be correct in order for this to work.

Tum
  • 5,019
  • 2
  • 20
  • 20
  • Could you share your code for this? It sounds like the best solution! – ebi Apr 07 '15 at 14:00
  • Added working code. Let me know if you have any questions about it! – Tum Apr 09 '15 at 07:54
  • 1
    Answer is good but Not Specify DataType of rows,columns,cellHeight,cellWidth. – Mihir Oza Jul 03 '15 at 13:05
  • `if(newIndex < items.count) {` Due to this line, app is crashing. So I updated it as following `if(newIndex < items.count-1) {` So if my array is having 4 objects, it displayed only 3 items in collection view. Any help please? – Bhumi Goklani Aug 12 '15 at 13:09
  • This was a really nice idea and easy to implement. – Felipe Ferri Oct 16 '16 at 14:25
  • Code is working with expected behaviors (Horizontal scroll with Horizontal cell filling) but this code will create multiple sections if no. of records are many. – Sandip Patel - SM Nov 17 '16 at 13:23
  • This works only when the collection view is not paged, e.g. when the number of elements is not greater than than rows*columns. – Balazs Vadnai Mar 03 '17 at 12:31
9

From @Erik Hunter, I post full code for make horizontal UICollectionView

UICollectionViewFlowLayout *collectionViewFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[collectionViewFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
self.myCollectionView.collectionViewLayout = collectionViewFlowLayout;

In Swift

let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .Horizontal
self.myCollectionView.collectionViewLayout = layout

In Swift 3.0

let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
self.myCollectionView.collectionViewLayout = layout

Hope this help

Subhash Khimani
  • 429
  • 7
  • 22
Linh
  • 43,513
  • 18
  • 206
  • 227
  • 1
    If your requirement for horizontal scroll with default direction to fill cell then it is fine (Default: Top to Bottom) But, if requirement is like to make horizontal scroll with cell filling horizontal (Left to Right) then need to customize it... (Refer : 5_Output_H-Scroll_H-Fill.png) – Sandip Patel - SM Jan 03 '17 at 05:40
8

We can do same Springboard behavior using UICollectionView and for that we need to write code for custom layout.

I have achieved it with my custom layout class implementation with "SMCollectionViewFillLayout"

Code repository:

https://github.com/smindia1988/SMCollectionViewFillLayout

Output as below:

1.png

First.png

2_Code_H-Scroll_V-Fill.png

enter image description here

3_Output_H-Scroll_V-Fill.png enter image description here

4_Code_H-Scroll_H-Fill.png enter image description here

5_Output_H-Scroll_H-Fill.png enter image description here

Sandip Patel - SM
  • 3,143
  • 25
  • 27
6

I am working on Xcode 6.2 and for horizontal scrolling I have changed scroll direction in attribute inspector.

click on collectionView->attribute inspector->scroll Direction->change to horizontal

enter image description here I hope it helps someone.

6

for xcode 8 i did this and it worked:

Gulz
  • 1,613
  • 15
  • 15
3

You can write a custom UICollectionView layout to achieve this, here is demo image of my implementation:

demo image

Here's code repository: KSTCollectionViewPageHorizontalLayout

@iPhoneDev (this maybe help you too)

Huy Nguyen
  • 1,881
  • 3
  • 22
  • 28
Zeng
  • 31
  • 3
0

If you need to set the UICollectionView scrolling Direction Horizental and you need to set cell width and height static. Please set the collectionview estimate size Automatic into None .

View The ScreenShot