How do I create a horizontal scrolling UICollectionView in Swift? - ios

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.

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)

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

Specify the height of the collection view and cell size. More details below:
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.
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)
}

Related

Collection view first cell size issue

i have to hide some of the cells of uicollectionview based on my api response.
i am setting cell size to zero in collectionview's sizeForItemAtIndexPath method based on my json response value.
but even after setting the size to zero the cell which is at 0th index is always visible.
i can not filter out and remove the data from the array. i need that data for some operations.
my UICollectionView datasource method.
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! testCell
cell.lblTest.text = "\(indexPath.item)"
return cell
}
my flow layout delegate method.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if (indexPath.section == 0 && indexPath.item == 0) || (indexPath.section == 1 && indexPath.item == 0){
return CGSize(width: 0, height: 0)
}
return CGSize(width: 50, height: 50)
}
expected result is the cell at 0th index in 0 and 1 section is hidden but it is still displayed.
You shouldn't do this by trying to set the size to 0. If you set it to 0 underlying code reads that as trying to automatically size it for you (this is why setting it to a small value close to 0 looks like it almost works). What you want to actually do is just adjust your data source. So when it asks for the number of items return the amount without the items that you want hidden, and cellForItem should just take into account that the item isn't there.
You have to set it to a number close to 0, such as 0.1, 0.01.

How to trigger didSelectItemAtIndexPath at select center cell once the scroll end in ios swift?

Here UPCarouselFlowLayout is used for carousel scroll. As of now, user must tap a cell in order to trigger collection view didSelectItemAtIndexPath. Is there a way to select the center cell once the scrolling ended automatically?
here is the code i used to carousel:
let layout = UPCarouselFlowLayout()
layout.itemSize = CGSize(width: 211, height: 75)
layout.scrollDirection = .horizontal
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 10)
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 65)
carCollection.collectionViewLayout = layout
here the code used for collection view:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return carCategory.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! carCollectionViewCell
cell.carName.text = carCategory[indexPath.row]
cell.carImage.image = UIImage(named: carCategoryImage[indexPath.row])
cell.carMeters.text = carCategoryMeter[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selected index::\(indexPath.row)")
}
If you look at ViewController.swift from the Demo included with ** UPCarouselFlowLayout**, you will see the function scrollViewDidEndDecelerating. That is triggered when the scroll stops moving and a cell become the "center" cell.
In that function, the variable currentPage is set, and that's where the labels below the collection view are changed.
So, that's one place to try what you want to do.
Add the two lines as shown here... when the scroll stops, you create an IndexPath and manually call didSelectItemAt:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let layout = self.collectionView.collectionViewLayout as! UPCarouselFlowLayout
let pageSide = (layout.scrollDirection == .horizontal) ? self.pageSize.width : self.pageSize.height
let offset = (layout.scrollDirection == .horizontal) ? scrollView.contentOffset.x : scrollView.contentOffset.y
currentPage = Int(floor((offset - pageSide / 2) / pageSide) + 1)
// add these two lines
let indexPath = IndexPath(item: currentPage, section: 0)
collectionView(self.collectionView, didSelectItemAt: indexPath)
}
You will almost certainly want to add some error checking and additional functionality (like only calling didSelect if the cell actually changed, as opposed to just sliding it a little but remaining on the current cell), but this is a starting point.

Unscrollable Multiple Collection Views

I am trying to develop an iOS application that has 03 UICollectionViews handled by the same ViewController. (The reason why I am opting for 03 UICollectionViews rather than one with sections and different prototype cells is because I may need to add additional content not relevant to collection views between the sections in the future)
________________
| _ _
| | | | |
| |_| |_| . . . (UICollectionView1)
|_______________
_______________
| _ _
| | | | |
| |_| |_| . . . (UICollectionView2)
|_______________
My problem is as follows:
No of cells in each of the CollectionViews is variable and if the number exceeds the width constraint it wraps down (so far so good). However, the height constraint of the UICollectionView causes a scroll bar to appear rather than simply laying out the cells if the number of cells causes to wrap beyond the height constraint
I have tried a couple of things to get this to work, most of which revolve around the advice given in the following questions
how to set dynamic height of a Collection View, the 'view' not the 'cells'?
How to adjust height of UICollectionView to be the height of the content size of the UICollectionView?
In the end I tried this
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.contentSize.height)
}
But then the content in the UICollectionViewCell stretched wierdly and still problem of scrolling exists.
I do not mind that the main view of the view controller (the one on which all other UICollectionViews are placed) becomes scrollable (actually that is part of the requirement), I just don't want the UICollectionViews to act like some HTML iframe and allow scrolling but just layout the cells in order for as much as constrained by width of the UICollectionView
In pseudo code, something like this
array = (cell1, cell2, cell3)
for i in array
if currentCollectionViewRow is filled
wrapToNextLine()
add cell{i} to view controller
Any help is appreciated. Even help that suggests better ways to achieve this functionality along with best practices rather than hacking code
EDIT
I carried out the instructions as #Saad Chaudhry mentioned but to no avail. My layout is as follows: CollectionView Layout
As you can see, the stack view encloses both collection views as suggested. Now the IDE gives the following complaints: Ambiguous Layout
I tried adding constraints intuitively, then tried the IDEs options to add constraints automatically to no avail. Most times, there is no data cells on the screen.
For more information, without stack view but just two collection views, I get the following simulation: Without Stack Views
And with stack views I get the simulation: With stack views and note that the second collection view is missing
If I then constraint the stack view at 0,0,0,0, this brings back the issues where each of the collection view heights are ambiguous. Providing heights causes scrolling "within that collection view" if the number of cells are large (from datasource).
I simply want all the black squares to be rendered first and then the yellow squares. The parent view may scroll and that's fine bu not the individual collection views
My code for the controller:
import UIKit
class DemoCollectionViewController: UICollectionViewController {
#IBOutlet weak var nonPriorityCollectionView: UICollectionView!
#IBOutlet weak var priorityCollectionView: UICollectionView!
private let reuseIdentifier = "priorityCell"
fileprivate let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
fileprivate var priorityItems = [PriorityItem]()
fileprivate let itemsPerRow: CGFloat = 3
private let nonPriorityReuseIdentifier = "nonPriorityCell"
fileprivate var nonPriorityItems = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: nonPriorityReuseIdentifier)
loadPriorityItems()
loadNonPriorityItems()
}
func loadPriorityItems(){
let item1 = PriorityItem(image: #imageLiteral(resourceName: "User"))
let item2 = PriorityItem(image: #imageLiteral(resourceName: "User"))
let item3 = PriorityItem(image: #imageLiteral(resourceName: "User"))
let item4 = PriorityItem(image: #imageLiteral(resourceName: "User"))
priorityItems = [item1, item2, item3, item4, item1, item2, item3, item4]
}
func loadNonPriorityItems(){
let item1 = "Item 1"
let item2 = "Item 2"
let item3 = "Item 3"
let item4 = "Item 4"
nonPriorityItems = [item1, item2, item3, item4]
}
}
// MARK: UICollectionViewDataSource
extension DemoCollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == self.priorityCollectionView {
return priorityItems.count
}
else{
return nonPriorityItems.count
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.priorityCollectionView {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = UIColor.black
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: nonPriorityReuseIdentifier, for: indexPath)
cell.backgroundColor = UIColor.yellow
return cell
}
}
}
extension DemoCollectionViewController : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let paddingSpace = sectionInsets.left * (itemsPerRow + 1)
let availableWidth = view.frame.width - paddingSpace
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return sectionInsets.left
}
}
Please help me in figuring out this issue. I now feel like collection view is not the ideal way to achieve this due to the complexity I'm facing...

When I scroll UICollectionView (paging enabled) too fast, it scrolls two pages

my qustion is simple.I have searched a lot and i'm familiar with UICollectionView but never saw this behavior of UICollectionView. please don't answer quickly and please read all my question.
I'm working on a project that uses collectionView (and auto layout). my collectionView is paging enabled and is working fine. but when I scroll it too fast (I mean very too fast) it scrolls two pages and shows the contents for next next row (previous previous row). also this code :
decelerationRate = UIScrollViewDecelerationRateFast
didn't work.
I created a temp project that have 1 controller and a simple collectionView (that is paging enabled and the cells's size are same as the viewController size). and there is just a label in the cell. the lable number is same as the collectionView row. also set this:
decelerationRate = UIScrollViewDecelerationRateFast
when I scroll it very fast from 1 to 2, it stops on 3 or vice versa.
this is my temp project code (that is auto layout):
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.sectionInset = UIEdgeInsets.zero
collectionViewLayout.minimumLineSpacing = 0
collectionViewLayout.minimumInteritemSpacing = 0
collectionViewLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
self.collView.collectionViewLayout = collectionViewLayout
self.collView.decelerationRate = UIScrollViewDecelerationRateFast
}
func numberOfSections(in collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollCell
print("cell For Item At Row \(indexPath.row)")
cell.lblNumber.text = "\(indexPath.row)"
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height)
}
anybody knows what should I do? is it a normal behavior for UICollectionView?

Dynamic UICollectionView in UIViewController

I am an android developer and developing my first iOS app, I need to implement GridView in order to add some social icon so after some search I found UICollectionView but it's not working as expected. How to set dynamic height of the UICollection view? Actually I want to display all icons but it showing only a row and others after scrolling the UICollectionView.
below is the example:
This is what I am getting
This is what I want:
I am using below code:
import UIKit
class CollectionViewDemo: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var socialHandleCollection: UICollectionView!
#IBOutlet weak var socialImageView: UIImageView!
var socialHandleArray:[String] = ["Facebook", "Twitter", "Youtube","Vimeo", "Instagram", "Custom URL", "Linkedin", "pinterest"]
override func viewDidLoad() {
self.socialHandleCollection.delegate = self
self.socialHandleCollection.dataSource = self
// socialHandleCollection.frame.size.height = 130
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return socialHandleArray.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: colvwCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! colvwCell
cell.imgCell.image = UIImage(named: "demo_img.png")
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print(self.socialHandleArray[indexPath.row])
}
}
Any help would be appreciated.
Assuming you are using autoLayout
Initially declare a specific number of columns that you desire
let numberOfColumns = 5
Okay so first things first. You will have to make your class conform to the UICollectionViewDelegateFlowLayout. Now implement the function -
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize{
return CGSize(width: collectionView.bounds.size.width/numberOfColumns, height: collectionView.bounds.size.width/numberOfColumns);
}
Here 5 can be changed to the number of columns you want in each row and passing same value as height will ensure that the shape is always square. And hence you can apply corner radius to make it circular.
Now moving on. From your interface builder, ctrl + click and drag the UICollectionView height constraint to your UIViewController (similar to how you would do for a UIView but do it for the constraint)
Now once you know the number of items that you need to display inside your UICollectionView, you can do something like:
//replace 5 with the number of columns you want
//array contains the items you wish to display
func figureOutHeight(){
if(array.count == 0){
//as no items to display in collection view
return
}
//ceil function is just to round off the value to the next one, for example 6/5 will return 1 but we need 2 in this case. Ensure all arguments inside ceil function are float
let rowsCount = ceil(Float(array.count)/Float(numberOfColumns))
//now you have number of rows soo just update your height constraint by multiplying row count with height of each item inside UICollectionView
collectionViewHeightConstraint.constant = rowsCount * collectionView.bounds.size.height / numberOfColumns;
view.layoutIfNeeded()
}
Also if you haven't change the scroll direction to vertical.

Resources