Uicollectionview - Horizontal Scroll, Horizontal Layout

UICollectionView - Horizontal scroll, horizontal layout?

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:

  • https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
  • http://www.objc.io/issue-3/collection-view-layouts.html

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.

UICollectionView horizontal scroll not working properly

Let's say you cell is 1000pts tall, you are setting the collection view cell height to 995 and setting the collection view top inset to 100 which returns 900.
So the problem is that your collectionView cell is 95pts taller than the collectionView.

You need to make your collectionView cell shorter or lower the collectionView top inset.

How do I create a horizontal scrolling UICollectionView in Swift?

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:

Sample Image

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

UICollectionView horizontal scrolling

Here's the UICollectionViewDelegateFlowLayout I used in my test project to achieve what you want.

func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width.multiplied(by: 0.9), height: 50.0)
}

// item spacing = vertical spacing in horizontal flow
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return (UIScreen.main.bounds.width.multiplied(by: 0.1))
}

// line spacing = horizontal spacing in horizontal flow
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return (UIScreen.main.bounds.width.multiplied(by: 0.1))
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: (UIScreen.main.bounds.width.multiplied(by: 0.1) / 2.0), bottom: 0, right: (UIScreen.main.bounds.width.multiplied(by: 0.1) / 2.0))
}

With your code it'd be like that:

fileprivate(set) lazy var collectionView: UICollectionView = {
let width = UIScreen.main.bounds.width.multiplied(by: 0.9)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: width, height: 50)

layout.sectionInset = UIEdgeInsets(top: 20, left: UIScreen.main.bounds.width.multiplied(by: 0.1) / 2.0, bottom: 10, right: UIScreen.main.bounds.width.multiplied(by: 0.1) / 2.0)
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = UIScreen.main.bounds.width.multiplied(by: 0.1)
layout.minimumInteritemSpacing = UIScreen.main.bounds.width.multiplied(by: 0.1) // or any value you want

let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: 50), collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .red
collectionView.isPagingEnabled = true
return collectionView
}()

Horizontal scroll in collection view

There are several methods that will notify you when a collection view gets scrolled, all of them are contained in UIScrollViewDelegate, and thus in UICollectionViewDelegate protocol, which inherits from the former.

  1. scrollViewDidScroll(_:) is indeed called each time the content offset changes. That means not only the active scrolling issued by the user, but also scrolling by inertia, programmatic scrolling and bouncing. You can use this method to react to the scrolling distance, for example, by querying the contentOffset property of the scroll view.
  2. scrollViewWillBeginDragging(_:) is, in contrast, getting called only at the beginning of scrolling issued by the user (that's why in the documentation you can see that this method may be called only after some delay, since the scroll view's gesture recognizer needs time to decide if it's a tap or a pan gesture). It will not be called again until the user lifts their finger and starts scrolling again.
  3. scrollViewWillBeginDecelerating(_:) is called when the user-issued scrolling discussed above ends, but the scroll view will continue scrolling further to achieve this inertia feeling. Again, it will not get called again until the user lifts their finger one more time.

That's basically it. If this still doesn't narrow down the event that you want to track (for example, you want to become notified when the user starts scrolling at the initial position only), you will need to set some flags or track additional properties.

To track horizontal scrolling for instance, you will need to either compare the scroll view's previous content offset to the current or check the scrolling velocity via scrollView.panGestureRecognizer.velocity(in: collectionView).

UICollectionView (Horizontal scrolling) Left to right & Top to bottom

I found that someone else shared the same issue and created a flow subclass called SMCollectionViewFillLayout. It does exactly what I intended with the following properties:

SMCollectionViewFillLayout *layout      =   [[SMCollectionViewFillLayout alloc] init];
layout.delegate = self;
layout.isHorizontalFill = YES;
layout.stretchesLastItems = NO;
layout.direction = SMCollectionViewFillLayoutHorizontal;
[self setCollectionViewLayout:layout];


Related Topics



Leave a reply



Submit