Changing UICollectionview cell XY position - swift 3 - ios

I'm using a UICollectionview with XX cells. How can i change the XY position on every single cell programmatically? I can change the size of every cell with:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
Is there another function like above that changes the XY positioning?

I hope this example will help you. Let me explain this code blocks. You can think standard UICollectionView and Cell design. There is no detailView. This code implementation will create parallax effect HeaderView.
FIRST AND FOREMOST
import UIKit
class DIYLayoutAttributes: UICollectionViewLayoutAttributes {
var deltaY: CGFloat = 0
override func copy(with zone: NSZone?) -> Any {
let copy = super.copy(with: zone) as! DIYLayoutAttributes
copy.deltaY = deltaY
return copy
}
override func isEqual(_ object: Any?) -> Bool {
if let attributes = object as? DIYLayoutAttributes {
if attributes.deltaY == deltaY {
return super.isEqual(object)
}
}
return false
}
}
class DIYLayout: UICollectionViewFlowLayout {
var maximumStretchHeight: CGFloat = 0
override class var layoutAttributesClass : AnyClass {
return DIYLayoutAttributes.self
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create layout attributes
let layoutAttributes = super.layoutAttributesForElements(in: rect) as! [DIYLayoutAttributes]
// How far user scrollview.
let insets = collectionView!.contentInset
let offset = collectionView!.contentOffset
let minY = -insets.top
// we need the calculate delta.
if (offset.y < minY) {
let deltaY = fabs(offset.y - minY)
// we can loop through all attributes
for attributes in layoutAttributes {
//Manipulate attributes
if let elementKind = attributes.representedElementKind {
if elementKind == UICollectionElementKindSectionHeader {
// create frame.
var frame = attributes.frame
frame.size.height = max(minY, headerReferenceSize.height + deltaY)
//We need the change origin for scrolling.
frame.origin.y = frame.minY - deltaY
attributes.frame = frame
attributes.deltaY = deltaY
}
}
}
}
return layoutAttributes
}
// Layout will scroll.
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
SECOND
You need custom cell design.
import UIKit
class ScheduleHeaderView: UICollectionReusableView {
#IBOutlet fileprivate weak var backgroundImageView: UIView!
#IBOutlet fileprivate weak var backgroundImageViewHeightLayoutConstraint: NSLayoutConstraint!
#IBOutlet fileprivate weak var foregroundImageView: UIView!
#IBOutlet fileprivate weak var foregroundImageViewHeightLayoutConstraint: NSLayoutConstraint!
fileprivate var backgroundImageViewHeight: CGFloat = 0
fileprivate var foregroundImageViewHeight: CGFloat = 0
fileprivate var previousHeight: CGFloat = 0
override func awakeFromNib() {
super.awakeFromNib()
backgroundImageViewHeight = backgroundImageView.bounds.height
foregroundImageViewHeight = foregroundImageView.bounds.height
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let attributes = layoutAttributes as! DIYLayoutAttributes
let height = attributes.frame.height
if previousHeight != height {
backgroundImageViewHeightLayoutConstraint.constant = backgroundImageViewHeight - attributes.deltaY
foregroundImageViewHeightLayoutConstraint.constant = foregroundImageViewHeight + attributes.deltaY
previousHeight = height
}
}
}
THIRD
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "ScheduleHeader", for: indexPath) as! ScheduleHeaderView
return header
}
Of course we need to from viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
//Add layout.
let width = collectionView!.bounds.width
let layout = collectionViewLayout as! DIYLayout
layout.headerReferenceSize = CGSize(width: width, height: 180)
layout.itemSize = CGSize(width: width, height: 62)
layout.maximumStretchHeight = width
}

Related

cellForItemAt called only once in Swift collectionView

If I use flow layout with collectionView, then all my cells are visible with the data. If I use a custom layout, then cellForItemAt is only accessed for index (0,0), and correspondingly only a single cell is displayed.
I'm baffled why - please help!
Minimal example below:
ViewController:
import UIKit
private let reuseIdentifier = "customCell"
class customCollectionViewController: UICollectionViewController {
#IBOutlet var customCollectionView: UICollectionView!
let dwarfArray = ["dopey", "sneezy", "bashful", "grumpy", "doc", "happy", "sleepy"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dwarfArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! customCollectionViewCell
let cellContentsIndex = indexPath.row
if cellContentsIndex <= dwarfArray.count
{
cell.displayContent(name: dwarfArray[cellContentsIndex])
}
return cell
}
}
Custom Cell
import UIKit
class customCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var nameLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect){
super.init(frame: frame)
}
public func displayContent(name: String){
nameLabel.text = name
}
func setup(){
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.black.cgColor
}}
Custom Layout
If this is not here - I can see all the cells I expect (albeit without my preferred layout). When I use this, I only see one cell.
import UIKit
class customCollectionViewLayout: UICollectionViewLayout {
let CELL_SIZE = 100.0
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
//define the size of the area the user can move around in within the collection view
var contentSize = CGSize.zero
var dataSourceDidUpdate = true
func collectionViewContentSize() -> CGSize{
return self.contentSize
}
override func prepare() {
if (collectionView?.numberOfItems(inSection: 0))! > 0 {
/// cycle through each item of the section
for item in 0...(collectionView?.numberOfItems(inSection: 0))!-1{
/// build the collection attributes
let cellIndex = NSIndexPath(item: item, section: 0)
let xPos = Double(item)*CELL_SIZE
let yPos = 40.0
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: cellIndex as IndexPath)
cellAttributes.frame = CGRect(x: xPos, y:yPos, width: CELL_SIZE, height: CELL_SIZE)
// cellAttributes.frame = CGRect(x: xPos, y:yPos, width: CELL_WIDTH + 2*CELL_SPACING, height: CELL_HEIGHT)
cellAttributes.zIndex = 1
//save
cellAttrsDictionary[cellIndex] = cellAttributes
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/// create array to hold all the elements in our current view
var attributesInRTect = [UICollectionViewLayoutAttributes]()
/// check each element to see if they should be returned
for cellAttributes in cellAttrsDictionary.values {
if rect.intersects(cellAttributes.frame)
{
attributesInRTect.append(cellAttributes)
}
}
return attributesInRTect
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath as NSIndexPath]!
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}}
Output
The problem is with contentSize value
func collectionViewContentSize() -> CGSize{
return self.contentSize
}
Just replace func collectionViewContentSize()... by something like this:
func lastLayoutAttributes() -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary.values.map { $0 }.sorted(by: { $0.frame.maxX < $1.frame.maxX }).last
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView else { return .zero }
guard collectionView.frame != .zero else { return .zero }
let width: CGFloat
let height: CGFloat = collectionView.frame.height
if let lastLayoutAttributes = lastLayoutAttributes() {
width = lastLayoutAttributes.frame.maxX
} else {
width = 0
}
return CGSize(width: width, height: height)
}
And you will see more than one cell.

Collectionview dynamic width not worked Swift

I have one collectionView inside Table view, it works perfectly but the issue is that collection view cell width not worked perfectly on initial stage but worked once scrolls it.
You can check here that in the first section, it shows full name but in other section, it truncates tail and that works after scrolls.
Here is the code that matters
class SubjectsViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
// use a value to keep track of left margin
var leftMargin: CGFloat = 0.0
for attributes in attributesForElementsInRect! {
let refAttributes = attributes
// assign value if next row
if (refAttributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
// set x position of attributes to current margin
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = leftMargin
if newLeftAlignedFrame.origin.x + newLeftAlignedFrame.size.width > (self.collectionView?.bounds.size.width)! {
leftMargin = 0.0
newLeftAlignedFrame.origin.x = 0.0
if (newAttributesForElementsInRect.last?.frame.origin.y == newLeftAlignedFrame.origin.y){
newLeftAlignedFrame.origin.y = newLeftAlignedFrame.origin.y + newLeftAlignedFrame.height + minimumLineSpacing
}
}
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current margin
leftMargin += refAttributes.frame.size.width + 10
newAttributesForElementsInRect.append(refAttributes)
}
return newAttributesForElementsInRect
}
}
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
if self.superview?.superview?.superview is UITableView {
(self.superview?.superview?.superview as! UITableView).beginUpdates()
(self.superview?.superview?.superview as! UITableView).endUpdates()
}
}
}
override var intrinsicContentSize: CGSize {
return collectionViewLayout.collectionViewContentSize
}
}
class TagTableCell: UITableViewCell {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: SubjectsViewFlowLayout!
override func awakeFromNib() {
super.awakeFromNib()
collectionView.register(UINib(nibName: "TagCollectionCell", bundle: nil), forCellWithReuseIdentifier: "TagCollectionCell")
}
func setupCell() {
let flowLayout = collectionView.collectionViewLayout as? SubjectsViewFlowLayout
flowLayout?.estimatedItemSize = .init(width: 100, height: 45)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.collectionView.reloadData()
}
}
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D, forRow row: Int) {
setupCell()
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = row + 1
collectionView.layoutIfNeeded()
}
}
You just need to call datasource and datadelegate in async method while adding the UICollectionView in tableview cell
DispatchQueue.main.async {
self.collectionView.delegate = dataSourceDelegate
self.collectionView.dataSource = dataSourceDelegate
}
You can modify your cell size just by adding this function, change your desired width and height for each cell.
Here is an example to make the cell's width half of the collection view's.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellWidth = collectionView.layer.bounds.width / 2
let cellHeight : CGFloat = 150
return CGSize(width: cellWidth, height: cellHeight)
}
You should implement this Protocol to your class:
UICollectionViewDelegateFlowLayout

pinch zoom in collection view

I am trying to add a pinch gesture zoom in my collection view so what is happening is the zoom is happening each time from the start it is not preserving the previous scale state and the max and min scale limit is also not working
since I am new to to swift I am struggling with this, thanks for the help in advance
here is my collection view controller class
import UIKit
class BookViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var seatMapCollection: UICollectionView!
#IBAction func pinchGesture(_ sender: UIPinchGestureRecognizer) {
}
var dateValue = ["1","2","3","4","10","6","7","8","9","5"]
var dayValue = ["Mon","Tue","Wed","Thu","Thu","Thu","Thu","Thu","Thu","Thu"]
var month = ["jan","feb","mar","apr","Thu","Thu","Thu","Thu","Thu","Thu"]
override func viewDidLoad() {
super.viewDidLoad()
self.seatMapCollection.dataSource = self
self.seatMapCollection.delegate = self
self.seatMapCollection.maximumZoomScale = 2
self.seatMapCollection.minimumZoomScale = 0.50
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(sender:)))
seatMapCollection.addGestureRecognizer(pinchGesture)
self.view.sendSubview(toBack: seatMapCollection)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//return seatmapCollection.numberOfItems(inSection: 150)
return 15
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SeatMapCollectionViewCell", for: indexPath) as! SeatMapCollectionViewCell
return cell
}
#objc func pinchAction(sender:UIPinchGestureRecognizer){
var scaleValue: CGFloat!
if sender.state == .changed || sender.state == .began {
if scaleValue == nil{
seatMapCollection.transform = CGAffineTransform.init(scaleX: sender.scale, y: sender.scale)
scaleValue = sender.scale
}else{
seatMapCollection.transform = CGAffineTransform.init(scaleX: scaleValue, y: scaleValue)
}
}
}
}
and here is my custom collection view layout class
import UIKit
class CustomCollectionViewLayout: UICollectionViewLayout {
let CELL_HEIGHT = 35.0
let CELL_WIDTH = 35.0
let STATUS_BAR = UIApplication.shared.statusBarFrame.height
var cache = [UICollectionViewLayoutAttributes]()
var contentSize = CGSize.zero
var cellAttrsDictionary = [UICollectionViewLayoutAttributes]()
var cellPadding = 5.0
override var collectionViewContentSize: CGSize {
return self.contentSize
}
var interItemsSpacing: CGFloat = 8
// 5
var contentInsets: UIEdgeInsets {
return collectionView!.contentInset
}
override func prepare() {
// 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!.numberOfItems(inSection: section) > 0 {
for item in 0...collectionView!.numberOfItems(inSection: section)-1 {
// Build the UICollectionVieLayoutAttributes for the cell.
let cellIndex = NSIndexPath(item: item, section: section)
let xPos = Double(item) * CELL_WIDTH
let yPos = Double(section) * CELL_HEIGHT
let frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
let cellFinalAttribute = frame.insetBy(dx:CGFloat(cellPadding) ,dy:CGFloat(cellPadding))
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: cellIndex as IndexPath)
cellAttributes.frame = cellFinalAttribute
cellAttrsDictionary.append(cellAttributes)
// 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
}
}
}
}
// 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)
}
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
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
}
here is my custom cell class
import UIKit
class SeatMapCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var seatDetail: UILabel!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.gray.cgColor
self.layer.cornerRadius = 5.0
}
}

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

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.

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