How do I create a horizontal scrolling UICollectionView in Swift?
Asked Answered
M

3

14

How can I make a horizontal scrolling collectionView that fills up cells going across the rows rather than down the columns?

I want there to 5 columns and 3 rows but when there is more than 15 items I want it to scroll to the next page. I'm having a lot of trouble getting this going.

Macronucleus answered 9/10, 2016 at 16:46 Comment(4)
Could you please give a picture example of what your collection view would look like if it had 23 items?Greedy
Added Picture to show what it would like like, 15 visible cells and if there were more the collection view would be scrolled to the side to view moreMacronucleus
I'm not sure what you're question is, you appear to have a horizontal layout. Are you asking how to make the view scroll horizontally?Despatch
The cells are layout in alphabetical order. In the picture the alphabetical order goes across the rows. In my app the alphabetical order is going down the columns. I want to know how i can maintain the horizontal scroll and make the order like in the pictureMacronucleus
G
10

Option 1 - Recommended

Use custom layouts for your collection view. This is the right way to do this and it gives you a lot of control over how you want your cells to fill the collection view.

Here is a UICollectionView Custom Layout Tutorial from "raywenderlich"


Option 2

This is more like a hackish way of doing what you want. In this method you can access your data source in an order to simulate the style you need. I'll explain it in the code:

var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInFirstPage = 5
// calculate number of columns needed to display all items
var columns: Int { return myArray.count<=columnsInFirstPage ? myArray.count : myArray.count > rows*columnsInFirstPage ? (myArray.count-1)/rows + 1 : columnsInFirstPage }

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        
    return columns*rows
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
    //These three lines will convert the index to a new index that will simulate the collection view as if it was being filled horizontally
    let i = indexPath.item / rows
    let j = indexPath.item % rows         
    let item = j*columns+i

    guard item < myArray.count else {
        //If item is not in myArray range then return an empty hidden cell in order to continue the layout
        cell.hidden = true
        return cell
    }
    cell.hidden = false

    //Rest of your cell setup, Now to access your data You need to use the new "item" instead of "indexPath.item"
    //like: cell.myLabel.text = "\(myArray[item])"

    return cell
}

Here is this code in action:

enter image description here

*The "Add" button just adds another number to myArray and reloads the collection view to demonstrate how it would look with different number of items in myArray


Edit - Group items into pages:

var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInPage = 5
var itemsInPage: Int { return columnsInPage*rows }
var columns: Int { return myArray.count%itemsInPage <= columnsInPage ? ((myArray.count/itemsInPage)*columnsInPage)  + (myArray.count%itemsInPage) : ((myArray.count/itemsInPage)+1)*columnsInPage }

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        
    return columns*rows
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)

    let t = indexPath.item / itemsInPage
    let i = indexPath.item / rows - t*columnsInPage
    let j = indexPath.item % rows      
    let item = (j*columnsInPage+i) + t*itemsInPage

    guard item < myArray.count else {
        cell.hidden = true
        return cell
    }
    cell.hidden = false

    return cell
}
Greedy answered 10/10, 2016 at 5:47 Comment(6)
Thanks for this great answer and explanation. Can you explain how i could make it go by pages, so on the first page it would start off 123456 on the first row 7,8.9,10,11,12 on the second row.. and then on the second page start with 19,20,21,22,23,24Macronucleus
@Macronucleus You're welcome:) I updated the answer to include grouping into pages.Greedy
@Macronucleus You can change columnsInPage = 5 to any number of columns that you want to group into a page.Greedy
Thanks so much worked perfectly, is there any way to fill the rest of the page with empty cells so that when the user swipes to the next page the whole first page shifts out. Really appreciate these great answers!!Macronucleus
@Macronucleus Glad to help:) yes sure, just change the return value of columns to (((myArray.count-1)/itemsInPage)+1)*columnsInPage this will display a full page even if there is just one cell in it.Greedy
@Macronucleus By the way you should consider using a pageController with each page containing a vertical collectionView. It might be exactly what you need.Greedy
J
29

Where you have a reference to your UICollectionViewFlowLayout(), just do:

layout.scrollDirection = .horizontal

Here is a nice tutorial for more info: https://www.youtube.com/watch?v=Ko9oNhlTwH0

Though for historical purposes, consider searching StackOverFlow quickly to make sure this isn't a duplicate.

Hope this helps.

Update: Your items will fill horizontally first and if there is not enough room within the collectionview going to the right, they will go to next row. So, start by increasing your collectionview.contentsize (should be larger the screen to enable scrolling) and then set your collectionview item (cell) size.

flowLayout.itemSize = CGSize(width: collectionView.contentSize.width/5, height: collectionView.contentSize.height/3)
Jounce answered 9/10, 2016 at 17:1 Comment(4)
i know how to make it horizontal, the problem is the cells filling down the columns rather than across the rowsMacronucleus
they are filling vertically first going down the columns and then going to the next row when the height is reachedMacronucleus
Check the above tutorial (written in Obj-C) but shows you methods to override for your collection view layout. This way you can customize pattern for defining frames based on item index.Jounce
The simplest solution is to shrink the height of the UICollectionView so that no more than one row can appear. Seriously.Olatha
G
10

Option 1 - Recommended

Use custom layouts for your collection view. This is the right way to do this and it gives you a lot of control over how you want your cells to fill the collection view.

Here is a UICollectionView Custom Layout Tutorial from "raywenderlich"


Option 2

This is more like a hackish way of doing what you want. In this method you can access your data source in an order to simulate the style you need. I'll explain it in the code:

var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInFirstPage = 5
// calculate number of columns needed to display all items
var columns: Int { return myArray.count<=columnsInFirstPage ? myArray.count : myArray.count > rows*columnsInFirstPage ? (myArray.count-1)/rows + 1 : columnsInFirstPage }

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        
    return columns*rows
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
    //These three lines will convert the index to a new index that will simulate the collection view as if it was being filled horizontally
    let i = indexPath.item / rows
    let j = indexPath.item % rows         
    let item = j*columns+i

    guard item < myArray.count else {
        //If item is not in myArray range then return an empty hidden cell in order to continue the layout
        cell.hidden = true
        return cell
    }
    cell.hidden = false

    //Rest of your cell setup, Now to access your data You need to use the new "item" instead of "indexPath.item"
    //like: cell.myLabel.text = "\(myArray[item])"

    return cell
}

Here is this code in action:

enter image description here

*The "Add" button just adds another number to myArray and reloads the collection view to demonstrate how it would look with different number of items in myArray


Edit - Group items into pages:

var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInPage = 5
var itemsInPage: Int { return columnsInPage*rows }
var columns: Int { return myArray.count%itemsInPage <= columnsInPage ? ((myArray.count/itemsInPage)*columnsInPage)  + (myArray.count%itemsInPage) : ((myArray.count/itemsInPage)+1)*columnsInPage }

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        
    return columns*rows
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)

    let t = indexPath.item / itemsInPage
    let i = indexPath.item / rows - t*columnsInPage
    let j = indexPath.item % rows      
    let item = (j*columnsInPage+i) + t*itemsInPage

    guard item < myArray.count else {
        cell.hidden = true
        return cell
    }
    cell.hidden = false

    return cell
}
Greedy answered 10/10, 2016 at 5:47 Comment(6)
Thanks for this great answer and explanation. Can you explain how i could make it go by pages, so on the first page it would start off 123456 on the first row 7,8.9,10,11,12 on the second row.. and then on the second page start with 19,20,21,22,23,24Macronucleus
@Macronucleus You're welcome:) I updated the answer to include grouping into pages.Greedy
@Macronucleus You can change columnsInPage = 5 to any number of columns that you want to group into a page.Greedy
Thanks so much worked perfectly, is there any way to fill the rest of the page with empty cells so that when the user swipes to the next page the whole first page shifts out. Really appreciate these great answers!!Macronucleus
@Macronucleus Glad to help:) yes sure, just change the return value of columns to (((myArray.count-1)/itemsInPage)+1)*columnsInPage this will display a full page even if there is just one cell in it.Greedy
@Macronucleus By the way you should consider using a pageController with each page containing a vertical collectionView. It might be exactly what you need.Greedy
S
1

Specify the height of the collection view and cell size. More details below:

  1. Set the constraints of the UICollectionView, pinning the edges. Be sure to specify the UICollectionView's height or constraints so it's clear the cells can only scroll horizontally and not go down to the next line. The height should be the same or slightly larger than the cell height you specify in step 2.

  2. Implement the UICollectionViewDelegateFlowLayout delegate and sizeForItemAt method. Here's a sample sizeForItemAt implementation.

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let cellWidth = 100
        let cellHeight = 30
        return CGSize(width: cellWidth, height: cellHeight)
    }
    
Stopoff answered 7/2, 2020 at 15:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.