Adding a HeaderView to CollectionView when using custom FlowLayout (Swift) - ios

I've implemented a custom FlowLayout subclass, which apparently has precluded me from adding a headerView via the storyboard (the check box is gone). Is there any other way to do so using storyboards?
There are some answers about how to add a headerView programmatically but they're in objective-C, how can I add one using Swift?
The below doesn't produce a header view and I can't figure out why?
CollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Setup Header
self.collectionView!.registerClass(PinHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: headerIdentifier)
}
override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
//1
switch kind {
//2
case UICollectionElementKindSectionHeader:
//3
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind,withReuseIdentifier: "PinHeaderView",
forIndexPath: indexPath)
as! PinHeaderView
headerView.pinHeaderLabel.text = boardName
return headerView
default:
//4
fatalError("Unexpected element kind")
}
}
}
class PinHeaderView: UICollectionReusableView {
#IBOutlet weak var pinHeaderLabel: UILabel!
}
My Layout class:
import UIKit
protocol PinterestLayoutDelegate {
// 1. Method to ask the delegate for the height of the image
func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath , withWidth:CGFloat) -> CGFloat
// 2. Method to ask the delegate for the height of the annotation text
func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat
func columnsForDevice() -> Int
}
class PinterestLayoutAttributes:UICollectionViewLayoutAttributes {
// 1. Custom attribute
var photoHeight: CGFloat = 0.0
var headerHeight: CGFloat = 0.0
// 2. Override copyWithZone to conform to NSCopying protocol
override func copyWithZone(zone: NSZone) -> AnyObject {
let copy = super.copyWithZone(zone) as! PinterestLayoutAttributes
copy.photoHeight = photoHeight
return copy
}
// 3. Override isEqual
override func isEqual(object: AnyObject?) -> Bool {
if let attributtes = object as? PinterestLayoutAttributes {
if( attributtes.photoHeight == photoHeight ) {
return super.isEqual(object)
}
}
return false
}
}
class PinterestLayout: UICollectionViewLayout {
//1. Pinterest Layout Delegate
var delegate:PinterestLayoutDelegate!
//2. Configurable properties
//moved numberOfColumns
var cellPadding: CGFloat = 6.0
//3. Array to keep a cache of attributes.
private var cache = [PinterestLayoutAttributes]()
//4. Content height and size
private var contentHeight:CGFloat = 0.0
private var contentWidth: CGFloat {
let insets = collectionView!.contentInset
return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)
}
override class func layoutAttributesClass() -> AnyClass {
return PinterestLayoutAttributes.self
}
override func prepareLayout() {
// 1. Only calculate once
//if cache.isEmpty {
// 2. Pre-Calculates the X Offset for every column and adds an array to increment the currently max Y Offset for each column
let numberOfColumns = delegate.columnsForDevice()
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth )
}
var column = 0
var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)
// 3. Iterates through the list of items in the first section
for item in 0 ..< collectionView!.numberOfItemsInSection(0) {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
// 4. Asks the delegate for the height of the picture and the annotation and calculates the cell frame.
let width = columnWidth - cellPadding*2
let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath , withWidth:width)
let annotationHeight = delegate.collectionView(collectionView!, heightForAnnotationAtIndexPath: indexPath, withWidth: width)
let height = cellPadding + photoHeight + annotationHeight + cellPadding
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
// 5. Creates an UICollectionViewLayoutItem with the frame and add it to the cache
let attributes = PinterestLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.photoHeight = photoHeight
attributes.frame = insetFrame
cache.append(attributes)
// 6. Updates the collection view content height
contentHeight = max(contentHeight, CGRectGetMaxY(frame))
yOffset[column] = yOffset[column] + height
column = column >= (numberOfColumns - 1) ? 0 : ++column
}
//}
}
override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = PinterestLayoutAttributes(forSupplementaryViewOfKind: elementKind, withIndexPath: indexPath)
attributes.headerHeight = 100.0
attributes.frame = (self.collectionView?.frame)!
return attributes
}
override func collectionViewContentSize() -> CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if CGRectIntersectsRect(attributes.frame, rect ) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}

You can add header view in storyboard, drag a "Collection Reusable View" and drop it inside the collectionView, then set its class and identifier in storyboard. Or you can register your custom header class programmatically as shown in your code.
self.collectionView!.registerClass(PinHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: headerIdentifier)
viewForSupplementaryElementOfKind delegate method is incomplete, case UICollectionElementKindSectionFooter isn't handled, do the same for footer view as what you did for header view. If you don't see it, set borderWidth of header view's layer to a value greater than 0, it might show up.

inside the prepareForLayout function add one more attributes object for the header. You can append it into the cache at the end like that:
// Add Attributes for section header
let headerAtrributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: IndexPath(item: 0, section: 0))
headerAtrributes.frame = CGRect(x: 0, y: 0, width: self.collectionView!.bounds.size.width, height: 50)
cache.append(headerAtrributes)

Why are you using PinterestLayoutAttributes(forCellWithIndexPath: indexPath) in method layoutAttributesForSupplementaryViewOfKind?
You should use PinterestLayoutAttributes(forSupplementaryViewOfKind elementKind: String,
withIndexPath indexPath: NSIndexPath).
You are generating layout attributes for cell instead of suplementary view. As a result your layout attributes don't contain right category type and view kind. That's why even if you register class for suplementary view - collection view doesn't layout its instances.

Related

Custom Layout for Collection view doesn't update

I am using the following code in order to have a UICollectionView with variable height for various cells:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return totalItems
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
totalItems -= 1
collectionView.deleteItems(at: [indexPath])
collectionView.collectionViewLayout.invalidateLayout()
}
}
extension InviteViewController : PinterestLayoutDelegate {
// 1. Returns the cell height
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
print("have: ", indexPath.item)
if(indexPath.item % 3 == 0) {
return 150
} else if(indexPath.item % 3 == 1) {
return 200
} else {
return 250
}
}
}
However I see that the layout doesn't update after the deletion of the UICollectionViewCell and I get errors as:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: {length = 2, path = 0 - 7}'
I am stuck and unable to understand why this happens and how to solve this. Please help me find a solution for the same. Thank you.
In case you want to have a look at the layout file I used, please have a look here:
import UIKit
protocol PinterestLayoutDelegate: class {
// 1. Method to ask the delegate for the height of the image
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class PinterestLayout: UICollectionViewLayout {
//1. Pinterest Layout Delegate
weak var delegate: PinterestLayoutDelegate!
//2. Configurable properties
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 3
//3. Array to keep a cache of attributes.
fileprivate var cache = [UICollectionViewLayoutAttributes]()
//4. Content height and size
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// 1. Only calculate once
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// 2. Pre-Calculates the X Offset for every column and adds an array to increment the currently max Y Offset for each column
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
// 3. Iterates through the list of items in the first section
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 4. Asks the delegate for the height of the picture and the annotation and calculates the cell frame.
let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// 5. Creates an UICollectionViewLayoutItem with the frame and add it to the cache
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6. Updates the collection view content height
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
I followed: https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest
override func prepare() {
// 1. Only calculate once
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
In this part delete guard cache.isEmpty == true,. It works.
extension CategoryPageVC: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
let rate = self.categoryData[indexPath.item].image.size.width / self.collectionView.frame.size.width * 2
return self.categoryData[indexPath.item].image.size.height / rate
}
}

Connecting Custom layout class to collection view in code

i have a small question about a custom layout class and i need to connect it to my collection view in code not by the story board because i create the whole project by code . any help, please ?
my main class
import UIKit
import Firebase
class UserProfileController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
//fetchUser()
collectionView?.dataSource = self
collectionView?.delegate = self
collectionView?.register(TestCell.self, forCellWithReuseIdentifier: mainCellId)
}
fileprivate let mainCellId = "mainCellId"
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: mainCellId, for: indexPath) as! TestCell
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
the custom layout class which need to connect it to the collection view in code not story board
import UIKit
struct UltraVisualLayoutConstants {
struct Cell {
static let standardHeight: CGFloat = 100
static let featuredHeight: CGFloat = 280
// static let standardWidth: CGFloat = 100
// static let featuredWidth: CGFloat = 280
}
}
class UltraVisualLayout: UICollectionViewLayout {
// amount which users need to scroll before featured cell changes\
let dragOffset: CGFloat = 180.0
var cache = [UICollectionViewLayoutAttributes]()
//return item index of current featured cell
var featuredItemIndex: Int {
get {
//use max to ensure that featureditemindex never be < 0
return max(0 , Int(collectionView!.contentOffset.y / dragOffset))
}
}
// returns value between 0 and 1 to represent how close the next cell becomes the featured cell
var nextItemPercentegeOffset: CGFloat {
get {
return (collectionView!.contentOffset.y / dragOffset) - CGFloat(featuredItemIndex)
}
}
// return the width of collection view
var width: CGFloat {
get {
guard let width = collectionView?.bounds.width else {return 0}
return width
}
}
// return the height of collection view
var height: CGFloat {
get {
guard let height = collectionView?.bounds.height else {return 0}
return height
}
}
//returns the number of items in the collection view
var numberOfItems: Int {
get {
return collectionView!.numberOfItems(inSection: 0)
}
}
// MARK: UICollectionViewLayout
// return the size of all content in collection view
override var collectionViewContentSize: CGSize {
let contentHeight = (CGFloat(numberOfItems) * dragOffset) + (height - dragOffset)
return CGSize(width: width, height: contentHeight)
}
override func prepare() {
cache.removeAll(keepingCapacity: false)
let standardHeight = UltraVisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltraVisualLayoutConstants.Cell.featuredHeight
var frame = CGRect(x: 0, y: 0, width: 0, height: 0)
var y: CGFloat = 0
for item in 0..<numberOfItems {
let indexPath = NSIndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.zIndex = item
var height = standardHeight
if indexPath.item == featuredItemIndex {
let yOffset = standardHeight * nextItemPercentegeOffset
y = collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems {
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentegeOffset, 0)
y = maxY - height
}
frame = CGRect(x: 0, y: y, width: width, height: height)
attributes.frame = frame
cache.append(attributes)
y = frame.maxY
}
}
// return all attributes in cache whose frame intersects with the rect passed to the method
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
// return true so that layout is continuously invalidated as the user scrolls
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
You cannot set the layout programmatically if you don't have an initializer. From the docs:
layout
The layout object to use for organizing items. The collection
view stores a strong reference to the specified object. Must not be
nil.
Since you seem to be using the storyboard, you'll have to set it there.
Once you create you layout class in code, it will show up when you click you collection view on the storyboard. The default layout will be set to Flow, when you change it to Custom, a new class field will show up. When you click that, it will list your layout class. You can choose it and set it from there.
However if you are initializing your storyboard programmatically, then you just need to pass it as a parameter to your initializer.
var collectionView = UICollectionView(frame: yourFrame, collectionViewLayout: customCollectionViewLayout) // pass your custom collection view instance

difficulty dealing with the reusability of the collection view cell.

I am trying to create a seating plan using the collection view. In that each seat will have a specific number that means that each cell will have a label that displays the seat number. I am using custom collection view layout to create a seat map and the seat map is 2 way scrollable. Every thing goes well untill the seat map gets scrolled. once the seat map is scrolled the values of the seat numbers are being changed. I know that it is because of the reusability of the collection view cells but i can't find a way out. I tried to search for answer in stack but nothing is working for me. please help..
here is my view controller:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
var seatNo = 0
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
// func for number of section in collection view
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
if indexPath.row == 0 {
seatNo = 1
}
cell.assignSeat(seat: seatNo)
seatNo += 1
cell.seatNumber.text = "\(cell.seat ?? 0)"
return cell
}
}
and here is my collection view cell :
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var seatNumber: UILabel!
var seat: Int?
override func prepareForReuse() {
seatNumber.text = ""
seat = 0
}
func assignSeat(seat: Int) {
self.seat = seat
}
}
and if needed here is the code for the custom Collection View Layout:
class CustomCollectionLayout: UICollectionViewLayout {
// var for cell height
var CELL_HEIGHT: Double!
//Variable for cell width
var CELL_WIDTH: Double!
// Variable for the status bar height
let STATUS_BAR = UIApplication.shared.statusBarFrame.height
// Array to store the cell attributes
var cache = [UICollectionViewLayoutAttributes]()
//variable to define the content size
var contentSize = CGSize.zero
// another variable to store the cell atributes
var cellAttrsDictionary = [UICollectionViewLayoutAttributes]()
// variable to store the cell padding
var cellPadding: Double!
// func that defines the collection view content size
override var collectionViewContentSize: CGSize{
return self.contentSize
}
// func to prepare the collection view
// how the cells are to be mapped is defined in this function
override func prepare() {
// assigning the values to the variables
CELL_HEIGHT = 44
CELL_WIDTH = 44
cellPadding = 2
// Cycle through each section of the data source.
if collectionView?.isDragging == false{
if collectionView!.numberOfSections > 0 {
for section in 0...collectionView!.numberOfSections-1 {
// Cycle through each item in the section.
if collectionView!.numberOfItems(inSection: section) > 0 {
for item in 0...collectionView!.numberOfItems(inSection: section)-1 {
// storing the index of the current cell
let cellIndex = NSIndexPath(item: item, section: section)
// defining the x and y coordinates for the other cells
let xPos = Double(item) * CELL_WIDTH
let yPos = Double(section) * CELL_HEIGHT
//creating the frame for the cell
let frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
//providing the padding
let cellFinalAttribute = frame.insetBy(dx:CGFloat(cellPadding) ,dy:CGFloat(cellPadding))
//storing the cellattributes in the array
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: cellIndex as IndexPath)
cellAttributes.frame = cellFinalAttribute
cellAttrsDictionary.append(cellAttributes)
}
}
}
}
// Update content size.
let contentWidth = Double(collectionView!.numberOfItems(inSection: 0)) * CELL_WIDTH
let contentHeight = Double(collectionView!.numberOfSections) * CELL_HEIGHT
self.contentSize = CGSize(width: contentWidth, height: contentHeight)
}
}
// func that returns the cell attributes for the elements that are visible in the screen
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for cellAttributes in cellAttrsDictionary {
if rect.intersects(cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
//func that returns the cell attributes for the indexpath
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath.row]
}
//this func call the prepare func if the user scrolls if returned true
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
}
Rather than using an array, why not say
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
let seatNo = (indexPath.row + 1) + (indexPath.section * 15)
cell.assignSeat(seat: seatNo)
cell.seatNumber.text = "\(cell.seat ?? 0)"
return cell
}
I haven't ran this code but the idea is to use
indexPath.row + 1
to get the seats position in its row.
then we can use
indexPath.section * 15 // (number of seats per row should be declared static)
to get the count of seats in the previous rows to this one. in the first row the section will be 0 so won't add anything but subsequent rows will be added correctly
I highly advise declaring the 15 and 10 at the top of your file to avoid magic numbers

Swift Collection View custom layout change cell width in cell side

Hello i have working collectionview custom layout codes but some cells i need to change cell width in cell side comming varible from json dynamicly.
My CustomCollectionViewLayout
import UIKit
public var CELL_HEIGHT = 22.0
public var CELL_WIDTH = 40.0 // HERE WIDTH SOME CELLS MUST BE 80
class CustomCollectionViewLayout: UICollectionViewLayout {
// Used for calculating each cells CGRect on screen.
// CGRect will define the Origin and Size of the cell.
let STATUS_BAR = UIApplication.sharedApplication().statusBarFrame.height
// Dictionary to hold the UICollectionViewLayoutAttributes for
// each cell. The layout attribtues will define the cell's size
// and position (x, y, and z index). I have found this process
// to be one of the heavier parts of the layout. I recommend
// holding onto this data after it has been calculated in either
// a dictionary or data store of some kind for a smooth performance.
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
// Defines the size of the area the user can move around in
// within the collection view.
var contentSize = CGSize.zero
// Used to determine if a data source update has occured.
// Note: The data source would be responsible for updating
// this value if an update was performed.
var dataSourceDidUpdate = true
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func prepareLayout() {
// Only update header cells.
if !dataSourceDidUpdate {
// Determine current content offsets.
let xOffset = collectionView!.contentOffset.x
let yOffset = collectionView!.contentOffset.y
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Confirm the section has items.
if collectionView?.numberOfItemsInSection(section) > 0 {
// Update all items in the first row.
if section == 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: item, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
// Also update x-position for corner cell.
if item == 0 {
frame.origin.x = xOffset
}
frame.origin.y = yOffset
attrs.frame = frame
}
}
// For all other sections, we only need to update
// the x-position for the fist item.
} else {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: 0, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
frame.origin.x = xOffset
attrs.frame = frame
}
}
}
}
}
// Do not run attribute generation code
// unless data source has been updated.
return
}
// Acknowledge data source change, and disable for next time.
dataSourceDidUpdate = false
var maxItemInASection: Int?
// Cycle through each section of the data source.
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Cycle through each item in the section.
if collectionView?.numberOfItemsInSection(section) > 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build the UICollectionVieLayoutAttributes for the cell.
let cellIndex = NSIndexPath(forItem: item, inSection: section)
let xPos = Double(item) * CELL_WIDTH
let yPos = Double(section) * CELL_HEIGHT
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
// Determine zIndex based on cell type.
if section == 0 && item == 0 {
cellAttributes.zIndex = 4
} else if section == 0 {
cellAttributes.zIndex = 3
} else if item == 0 {
cellAttributes.zIndex = 2
} else {
cellAttributes.zIndex = 1
}
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
if maxItemInASection < item {
maxItemInASection = item
}
}
}
}
}
// Update content size.
let contentWidth = Double(maxItemInASection ?? 0) * CELL_WIDTH
let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
self.contentSize = CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for cellAttributes in cellAttrsDictionary.values.elements {
if CGRectIntersectsRect(rect, cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath]!
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
}
And here my cell codes.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("customCell", forIndexPath: indexPath) as! CustomCollectionViewCell
if items[indexPath.section].base == 2 {
cell.label.text = items[indexPath.section].base
cell.frame.size.width = 80
cell.layer.frame.size.width = 80
CELL_WIDTH = 80.0 // HERE I CHANGED SOME CELLS WIDTH 80 BUT DONT CHANGE THEM !!
}else{
cell.label.text = items[indexPath.section].base
}
You can see top side
CELL_WIDTH = 80.0 // HERE I CHANGED SOME CELLS WIDTH 80 BUT DONT CHANGE THEM !!
You could implement collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) instead of subclassing UICollectionViewLayout - unless you have other reasons to do that.
It would go somewhat like:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if indexPath.item % 3 == 0 { // <-- your condition here
return CGSize(width: 40, height: 22)
}
return CGSize(width: 80, height: 22)
}
By the way, don't forget to set your collectionView's delegate in order for this method to be called. 😁
UPDATED
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if items[indexPath.section].base == 2 {
return CGSize(width: 80, height: 22)
} else {
return CGSize(width: 44, height: 22)
}
}
This is how I think your method should look like.
At your cellForItem you only need to worry about setting the labels, let the above code take care of the size of the cells.
sizeForItemAtIndexPath is not present for UICollectionViewLayout. So your CustomCollectionViewLayout inherited from UICollectionViewLayout does not have a protocol. You can create a protocol widthForItemAtIndexPath method.
protocol CustomCollectionViewDelegateLayout: NSObjectProtocol {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, widthForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat
}
In your viewController implemented this protocol method. Return value will be the width for the given indexPath
extension CustomCollectionViewController: CustomCollectionViewDelegateLayout {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, widthForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//from indexPath.section and indexPath.item identify double width item
if sirano == 2 && satimdurum == 0 {
return regularWidth * 2
}
return regularWidth
}
}
In prepareLayout() method of your CustomCollectionViewLayout you can calculate position of each item you need to check which item should be double.
guard let width = delegate?.collectionView(collectionView!, layout: self, widthForItemAtIndexPath: cellIndex) else {
print("Please conform to CustomCollectionViewDelegateLayout protocol")
return
}
Made a demo project with Xcode 7.2 here at https://github.com/rishi420/UICollectionviewsample
Do not change cell size inside the cellForItemAtIndexPath method. The cell layout is already set after dequeueReusableCellWithReuseIdentifier.
Change it in the sizeForItemAtIndexPath. This is called before the above delegate method.

Horizontal Flow for UICollectionView

I want to implement something like this:
I use a collection view with an horizontal flow and custom cell to set the size (every cell has a different size based on the text). The problem is that the collection view is like a matrix and when there is a small element and a big element on the same column, there will be a bigger space between elements from the same line.
Now I have something like this:
Is there any solution to do this with collection view? Or should I use scroll view instead?
Thank you!
For Flow layout Swift
override func viewDidLoad() {
var flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSizeMake(view.frame.width - 10, 10)
flowLayout.minimumLineSpacing = 2
flowLayout.minimumInteritemSpacing = 2
flowLayout.sectionInset = UIEdgeInsetsMake(2, 2, 0, 0)
collectionView.dataSource=self
collectionView.delegate=self
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: 90, height: 50) // The size of one cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSizeMake(self.view.frame.width, 0) // Header size
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
let frame : CGRect = self.view.frame
let margin: CGFloat = 0
return UIEdgeInsetsMake(0, margin, 0, margin) // margin between cells
}
You will have to write Your own custom flow layout subclass or use an already written one ( third party ). The main functions You need to consider for overriding ( from UICollectionViewLayout ) are :
-(void)prepareLayout
-(CGSize)collectionViewContentSize
-(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
-(UICollectionViewLayoutAttributes *) layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
Also keep in mind that when You use the UICollectionViewFlowLayout the horizontal space between the items is mainly controller from :
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
This is the solution that worked for me:
class CustomHorizontalLayout: UICollectionViewFlowLayout
{
var _contentSize = CGSizeZero
var itemAttibutes: NSMutableArray = NSMutableArray()
var delegate: CustomHorizontalLayoutDelegate!
override func prepareLayout() {
super.prepareLayout()
self.itemAttibutes = NSMutableArray()
// use a value to keep track of left margin
var upLeftMargin: CGFloat = self.sectionInset.left;
var downLeftMargin: CGFloat = self.sectionInset.left;
let numberOfItems = self.collectionView!.numberOfItemsInSection(0)
for var item = 0; item < numberOfItems; item++ {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
let refAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
let size = delegate.collectionView(collectionView!, sizeTagAtIndexPath: indexPath)
if (refAttributes.frame.origin.x == self.sectionInset.left) {
upLeftMargin = self.sectionInset.left
downLeftMargin = self.sectionInset.left
} else {
// set position of attributes
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = (indexPath.row % 2 == 0) ? upLeftMargin :downLeftMargin
newLeftAlignedFrame.origin.y = (indexPath.row % 2 == 0) ? 0 :40
newLeftAlignedFrame.size.width = size.width
newLeftAlignedFrame.size.height = size.height
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current margin
if(indexPath.row % 2 == 0)
{
upLeftMargin += refAttributes.frame.size.width + 8
}
else
{
downLeftMargin += refAttributes.frame.size.width + 8
}
self.itemAttibutes.addObject(refAttributes)
}
_contentSize = CGSizeMake(max(upLeftMargin, downLeftMargin), 80)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.itemAttibutes.objectAtIndex(indexPath.row) as? UICollectionViewLayoutAttributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let predicate = NSPredicate { (evaluatedObject, bindings) -> Bool in
let layoutAttribute = evaluatedObject as! UICollectionViewLayoutAttributes
return CGRectIntersectsRect(rect, layoutAttribute.frame)
}
return (itemAttibutes.filteredArrayUsingPredicate(predicate) as! [UICollectionViewLayoutAttributes])
}
override func collectionViewContentSize() -> CGSize {
return _contentSize
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return false
}
}
Here is the solution it works fine for a flexible width. Create a new Swift.File paste the code
and then implement the delegate Method in your ViewController.
import UIKit
protocol CameraLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, widthForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class CameraLayout: UICollectionViewLayout {
var delegate: CameraLayoutDelegate!
var numberOfRows = 2
private var cache = [UICollectionViewLayoutAttributes]()
private var contentWidth: CGFloat = 0
private var height: CGFloat {
get {
return (collectionView?.frame.size.height)!
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: height)
}
override func prepare() {
if cache.isEmpty {
let columumnsHeight = (height / CGFloat(numberOfRows))
var yOffset = [CGFloat]()
for column in 0..<numberOfRows {
yOffset.append(CGFloat(column) * (columumnsHeight + 8))
}
var xOffset = [CGFloat](repeating: 0, count: numberOfRows)
var column = 0
for item in 0..<collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = delegate.collectionView(collectionView!, widthForPhotoAtIndexPath: indexPath)
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: width, height: columumnsHeight)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
contentWidth = max(contentWidth, frame.maxX)
xOffset[column] = xOffset[column] + width + 8
column = column >= (numberOfRows - 1) ? 0 : column + 1
//
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes] ()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
#
ViewController:
let layout = CameraLayout()
layout.delegate = self
collectionView.collectionViewLayout = layout
extension ViewController: CameraLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, widthForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
return .........
}
}
Try to use SKRaggyCollectionViewLayout. Set your collectionView layout class to SKRaggyCollectionViewLayout and connect it:
#property (nonatomic, weak) IBOutlet SKRaggyCollectionViewLayout *layout;
And set the properties of it:
self.layout.numberOfRows = 2;
self.layout.variableFrontierHeight = NO;
https://github.com/tralf/SKRaggyCollectionViewLayout

Resources