Desire Layout on CollectionView
I having trouble achieving a custom layout on collectionview in swift 3.0. Its been two days searching on similar layout but I couldn't find one. Pls anyone help me to achieve my desire custom layout.
There are many ways of doing and there are many 3rd party lib to help out. But I suspect you don't know how to do layout and spacing among cell.
Here is the best help ..just play with values to get your similar layout.
override func prepareLayout() {
// 1
if cache.isEmpty {
// 2
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
for item in 0 ..< collectionView!.numberOfItemsInSection(0) {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
// 4
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
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
contentHeight = max(contentHeight, CGRectGetMaxY(frame))
yOffset[column] = yOffset[column] + height
column = column >= (numberOfColumns - 1) ? 0 : ++column
}
}
}
Reference UIcollectionView Layout
Related
Inspired by this tutorial [Custom UICollectionViewLayout Tutorial With Parallax][1]
[1]: https://www.raywenderlich.com/527-custom-uicollectionviewlayout-tutorial-with-parallax I created my own version of parallax collection view.
I am creating the parallax animation and bounce affect on the header, and footer with this logic:
private func updateSupplementaryViews(_ type: Element, attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
if type == .header,
settings.isHeaderStretchy {
var delta: CGFloat = 0.0
let updatedHeight = min(
collectionView.frame.height + headerSize.height,
max(headerSize.height , headerSize.height - contentOffset.y))
let scaleFactor = updatedHeight / headerSize.height
if contentOffset.y <= 0{
delta = (updatedHeight - headerSize.height) / 2
}else{
delta = (headerSize.height - updatedHeight - abs(contentOffset.y))
}
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let translation = CGAffineTransform(translationX: 0, y: min(contentOffset.y, headerSize.height) + delta)
attributes.transform = scale.concatenating(translation)
}else if type == .footer,
settings.isFooterStretchy {
var updatedHeight: CGFloat = 0.0
if collectionView.contentSize.height - contentOffset.y < UIScreen.main.bounds.height {
updatedHeight = min(
collectionView.frame.height + footerSize.height,
max(footerSize.height, abs(((collectionView.contentSize.height - (footerSize.width + deviationDeviceSize)) - UIScreen.main.bounds.height) - contentOffset.y)))
}else{
updatedHeight = min(
collectionView.frame.height + footerSize.height,
max(headerSize.height, headerSize.height - contentOffset.y))
}
let scaleFactor = updatedHeight / footerSize.height
let delta = ((updatedHeight - footerSize.height) / 2) - footerSize.height
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let translation = CGAffineTransform(translationX: 0, y: min(contentOffset.y, footerSize.height) + delta )
attributes.transform = scale.concatenating(translation)
}
}
Everything works great beside the annoying fact that all my cell subviews are scaling with this animation and I would like to scale only my background image. This seems simple enough but unfortunately I couldn't figure out a way to control what subviews are being manipulated by the transformation.
What have you set as customAttributes for Element.footer ?
What is the footerSize you are setting ?
You have included some implementations around footer in your project which is not visible for us? Mind to share the whole file ?
I tried this solution here but it seems to only work for vertical layout. I'm trying to make it work for a horizontal layout. In my case, I always want 3 cells on top and 2 on bottom that is center aligned.
Example:
I think this should help you a bit (I defined insets in delegate method because collectionview would otherwise center only my 1st section and leave others untouched):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// handling layout
// which needs to be centered vertically and horizontally
// also:
// maximum number of items = 6
// maximum number of rows = 2
// maximum number of items in row = 3
let numberOfItems: CGFloat
let numberOfRows: CGFloat
if collectionView.numberOfItems(inSection: section) > Constants.maxNumberOfItemsInRow {
numberOfItems = CGFloat(Constants.maxNumberOfItemsInRow)
numberOfRows = CGFloat(Constants.maxNumberOfRows)
} else {
numberOfItems = CGFloat(collectionView.numberOfItems(inSection: section))
numberOfRows = CGFloat(Constants.minNumberOfRows)
}
let totalCellWidth = Constants.itemSize.width * numberOfItems
let totalSpacingWidth = Constants.minimumInteritemSpacing * numberOfItems
var leftInset = (collectionView.layer.frame.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let totalCellHeight = Constants.itemSize.height * numberOfRows
let maximumSectionHeight = (Constants.itemSize.height * CGFloat(Constants.maxNumberOfRows)) + (CGFloat(Constants.maxNumberOfRows + 1) * Constants.minimumLineSpacing)
if leftInset < 0.0 { leftInset = 0.0 }
let topInset = (maximumSectionHeight - totalCellHeight) / 2
let rightInset = leftInset
return UIEdgeInsets(top: topInset, left: leftInset, bottom: topInset, right: rightInset)
}
Also, layout is defined by class:
class CollectionViewFlowLayout: UICollectionViewFlowLayout {
// MARK: - Constants
private struct Constants {
static let minimumInteritemSpacing: CGFloat = 12.5
static let minimumLineSpacing: CGFloat = 16.5
static let itemSize = CGSize(width: 64.0, height: 90.0)
}
override func prepare() {
guard let collectionView = collectionView else { return }
/// Defining flow layout for collectionview presentation
itemSize = Constants.itemSize
headerReferenceSize = CGSize(width: collectionView.dc_width, height: 1)
scrollDirection = .vertical
minimumInteritemSpacing = Constants.minimumInteritemSpacing
minimumLineSpacing = Constants.minimumLineSpacing
super.prepare()
}
}
I downloaded this Pintrest-esc Custom Layout for my CollectionView from : https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2 (modified it a little bit to do with cache code)
So the layout works fine, i got it set up so when you scroll to the bottom, i make another API call, create and populate additional cells.
Problem: If a User scrolls down and loads some more cells (lets say 20 for example) and then uses the "Search Feature" i have implemented at the top, it will then filter some of the data via price from MinRange - MaxRange leaving you with (lets say 5 results), thus deleting some cells and repopulating those. BUT - All that space and scrollable space that existed for the first 20 cells still exists as blank white space.... How can i remove this space ? and why can it resize when adding more cells, but not when removing them ?
Code of my API Call:
DispatchQueue.global(qos: .background).async {
WebserviceController.GetSearchPage(parameters: params) { (success, data) in
if success
{
if let products = data!["MESSAGE"] as? [[String : AnyObject]]
{
self.productList = products
}
}
DispatchQueue.main.async(execute: { () -> Void in
self.pintrestCollectionView.reloadData()
self.pintrestCollectionView.collectionViewLayout.invalidateLayout()
self.pintrestCollectionView.layoutSubviews()
})
}
}
Code Of Custom Layout Class & protocol:
protocol PinterestLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class PintrestLayout: UICollectionViewFlowLayout {
// 1
weak var delegate : PinterestLayoutDelegate!
// 2
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 10
// 3
fileprivate var cache = [UICollectionViewLayoutAttributes]()
// 4
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)
}
// 5
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// 1
//You only calculate the layout attributes if cache is empty and the collection view exists.
/*guard cache.isEmpty == true, let collectionView = collectionView else {
return
}*/
cache.removeAll()
guard cache.isEmpty == true || cache.isEmpty == false, let collectionView = collectionView else {
return
}
// 2
//This declares and fills the xOffset array with the x-coordinate for every column based on the column widths. The yOffset array tracks the y-position for every 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
//This loops through all the items in the first section, as this particular layout has only one section.
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
if collectionView.numberOfItems(inSection: 0) < 3
{
collectionView.isScrollEnabled = false
} else {
collectionView.isScrollEnabled = true
}
let indexPath = IndexPath(item: item, section: 0)
// 4
//This is where you perform the frame calculation. width is the previously calculated cellWidth, with the padding between cells removed.
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: 7, dy: 4)
collectionView.contentInset = UIEdgeInsets.init(top: 15, left: 12, bottom: 0, right: 12)
// 5
//This creates an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
//This expands contentHeight to account for the frame of the newly calculated item.
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
}
FYI: In MainStoryboard, i disabled "Adjust Scroll View Insets" as many of other threads and forums have suggested...
I'm guessing you've already found the answer, but I'm posting here for the next person since I ran into this issue.
The trick here is contentHeight actually never gets smaller.
contentHeight = max(contentHeight, frame.maxY)
This is a nondecreasing function.
To fix it, just set contentHeight to 0 if prepare is called:
guard cache.isEmpty == true || cache.isEmpty == false, let
collectionView = collectionView else {
return
}
contentHeight = 0
Image on right is what I try to achieve
Does anyone know how I could achieve this on a two column UICollectionView ?
I'm able to discern my columns by testing if (indexPath.row % 2 = 0) but I can't manage to achieve this result.
Is their a simple way to do so ? Or would I need a custom UICollectionViewFlowLayout ?
Note : All my cells are the same size.
Yes, you need to create a custom UICollectionViewLayout and that's simple to achieve. All you need is the following:
prepare(): Perform the up-front calculations needed to provide layout information
collectionViewContentSize: Return the overall size of the entire content area based on your initial calculations
layoutAttributesForElements(in:): Return the attributes for cells and views that are in the specified rectangle
Now that we have cells of the same size it's pretty easy to achieve.
Let's declare few variables to use:
// Adjust this as you need
fileprivate var offset: CGFloat = 50
// Properties for configuring the layout: the number of columns and the cell padding.
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 10
// Cache the calculated attributes. When you call prepare(), you’ll calculate the attributes for all items and add them to the cache. You can be efficient and
fileprivate var cache = [UICollectionViewLayoutAttributes]()
// Properties to store the content 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)
}
Next let's override the prepare() method
override func prepare() {
// If cache is empty and the collection view exists – calculate the layout attributes
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// xOffset: array with the x-coordinate for every column based on the column widths
// yOffset: array with the y-position for every column, Using odd-even logic to push the even cell upwards and odd cells down.
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat]()
for i in 0..<numberOfColumns {
yOffset.append((i % 2 == 0) ? 0 : offset)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// Calculate insetFrame that can be set to the attribute
let cellHeight = columnWidth - (cellPadding * 2)
let height = cellPadding * 2 + cellHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// Update the contentHeight to account for the frame of the newly calculated item. It then advances the yOffset for the current column based on the frame
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
Last we need collectionViewContentSize and layoutAttributesForElements(in:)
// Using contentWidth and contentHeight from previous steps, calculate collectionViewContentSize.
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
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]
}
You can find gist with the class.
This is how it appears:
Note: It is just a demo, you may have to tweak it according to your needs.
Hope It Helps!
how can I get the actual interItemSpacing for a horizontal flow layout in the UICollectionView ? the only info I can get (and set) is minimumInteritemSpacing.
I need it for the sake of the following small calculation to always display 3 equally width items in the collection view regardless to screen size (i want to replace minimumInteritemSpacing with the current value of interItemSpacing in the formula):
flowLayout.itemSize = CGSizeMake((_collectionView.frame.size.width/3.0)-(flowLayout.minimumInteritemSpacing/3.0), _collectionView.frame.size.height); I put this calculation in viewDidLayoutSubviews and it's not working precisely for some screens because their current interItemSpacing is different than the minimumInteritemSpacing
thanks in advance.
You could get the actual spacing by comparing two sequential cells
Something like
let firstIndex = IndexPath(item: 0, section: 0)
let secondIndex = IndexPath(item: 1, section: 0)
if let att1 = collectionView.layoutAttributesForItem(at: firstIndex),
let att2 = collectionView.layoutAttributesForItem(at: secondIndex) {
let actualSpace = att2.frame.minX - att1.frame.maxX
print (actualSpace)
}
For creating cells with fixing spacing try this:
private let minItemSpacing: CGFloat = 8
private let itemWidth: CGFloat = 100
private let headerHeight: CGFloat = 32
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Create our custom flow layout that evenly space out the items, and have them in the center
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
layout.minimumInteritemSpacing = minItemSpacing
layout.minimumLineSpacing = minItemSpacing
layout.headerReferenceSize = CGSize(width: 0, height: headerHeight)
// Find n, where n is the number of item that can fit into the collection view
var n: CGFloat = 1
let containerWidth = collectionView.bounds.width
while true {
let nextN = n + 1
let totalWidth = (nextN*itemWidth) + (nextN-1)*minItemSpacing
if totalWidth > containerWidth {
break
} else {
n = nextN
}
}
// Calculate the section inset for left and right.
// Setting this section inset will manipulate the items such that they will all be aligned horizontally center.
let inset = max(minItemSpacing, floor( (containerWidth - (n*itemWidth) - (n-1)*minItemSpacing) / 2 ) )
layout.sectionInset = UIEdgeInsets(top: minItemSpacing, left: inset, bottom: minItemSpacing, right: inset)
collectionView.collectionViewLayout = layout
}
For more information, this excellent article:
http://samwize.com/2015/11/30/understanding-uicollection-flow-layout/