Custom UICollectionViewFlowLayout not working - ios

I am trying to do a custom Flow Layout similar to the Apple News app. The flowLayout.delegate = self is in the viewDidLoad() method while my networking code is in in an async method in the viewDidAppear().
The problem is that the methods for the custom flow Layout get called before I can retrieve all the data from the server, therefore the app crashes.
Any ideas on how I could make it work? Here's my ViewController implementation:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var newsArray = [News]()
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://url"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Filling the cells with the correct info...
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
return getFromDb.news[indexPath.row].cellType
}
}
Here below the struct for News:
struct News {
let image: String
let provider: String
let title: String
let cellType: NewsCellType
init(image: String, provider: String, title: String, cellType: NewsCellType) {
self.image = image
self.provider = provider
self.title = title
self.cellType = cellType
}}
The Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let screenWidth = UIScreen.main.bounds.width
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
return attributes
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if attributesArray == nil {
attributesArray = [UICollectionViewLayoutAttributes]()
print(collectionView!.numberOfItems(inSection: 0) - 1)
for i in 0 ... collectionView!.numberOfItems(inSection: 0) - 1
{
let attributes = self.layoutAttributesForItem(at: IndexPath(item: i, section: 0))
attributesArray!.append(attributes!)
}
}
return attributesArray
}
}

Two things which you need to do when we deal with custom collection view flow layouts.
changes in prepare() method in custom flow layout. you will start preparing the layout only if the number of items more than 0.
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{ }
}
use numberOfItems property whenever you wanted to do something with collectionView items in the customFlowLayout to avoid crashes.

Here below the functioning clean code if you are looking to implement a Custom Collection View Flow Layout similar to the Apple News app with networking:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://yourURL"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
print("RELOAD")
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let news = getFromDb.news[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: news.cellType.rawValue, for: indexPath) as! NewCell
cell.image.image = UIImage(named: "news")!
cell.provider.text = news.provider
cell.title.text = news.title
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
print(getFromDb.news[indexPath.row].cellType)
return getFromDb.news[indexPath.row].cellType
}
}
This is the Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var cache = [UICollectionViewLayoutAttributes]()
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let screenWidth = UIScreen.main.bounds.width
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
cache.append(attributes)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attribute in cache{
if attribute.frame.intersects(rect){
layoutAttributes.append(attribute)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
}

Related

How to smoothly animate transition cells in uicollectionview in custom flowLayout

I want to create an app that shows an array of photos with captions (for now I have empty circles) that are arranged in horizontal uicollectionview. I want to make one larger circle to be displayed in the center of the screen, but the remaining circles in the line to be smaller and shaded. I did it by creating my own layout for uicollectionview. But I have one problem that I don't know how to fix it. I want the transition between circles to be smoothly animated. Here is an example of what I've done:
I want to achieve something like this:gif
I know that there are some library and frameworks which do that, but I'd like to do by my own.
Here is my code:
class FlowViewController: UIViewController {
let cellWidth : CGFloat = 275
let cellHeight : CGFloat = 300
let cellSpacing : CGFloat = 10
private let circleCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
private let layout = myCarouselFlowLayout()
private let collectionViewCellIdentifier = "PositionCollectionViewCell"
private var circles : [myCircle] = {
let pok1 = myCircle(name: "Bl4")
let pok2 = myCircle(name: "Bl3")
let pok3 = myCircle(name: "Bli")
let pok4 = myCircle(name: "Bl0")
let pok5 = myCircle(name: "Lal")
let pok6 = myCircle(name: "Te")
let pok7 = myCircle(name: "wTW")
let pok8 = myCircle(name: "H3RHG")
return [pok1, pok2, pok3, pok4, pok5, pok6, pok7, pok8]
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = #colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1)
view.addSubview(circleCollectionView)
configureCollectionView()
print(circles.count)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configureCollectionViewLayoutItemSize()
}
func configureCollectionView(){
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: cellWidth, height: cellHeight)
layout.minimumLineSpacing = cellSpacing
//set layout
circleCollectionView.setCollectionViewLayout(layout, animated: true)
//set delegates
setTableViewDelegates()
//register cells
circleCollectionView.register(CircleCollectionViewCell.self, forCellWithReuseIdentifier: collectionViewCellIdentifier)
//set contraits
circleCollectionView.translatesAutoresizingMaskIntoConstraints = false
circleCollectionView.heightAnchor.constraint(equalToConstant: cellHeight).isActive = true
circleCollectionView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
circleCollectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
circleCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circleCollectionView.backgroundColor = .clear
circleCollectionView.decelerationRate = .fast
}
func calculateSectionInset() -> CGFloat { // should be overridden
let viewWith = UIScreen.main.bounds.size.width
return ( viewWith - cellWidth ) / 2
}
private func configureCollectionViewLayoutItemSize() {
let inset: CGFloat = calculateSectionInset()
layout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
layout.itemSize = CGSize(width: layout.collectionView!.frame.size.width - inset * 2,
height: layout.collectionView!.frame.size.height)
}
}
extension FlowViewController: UICollectionViewDelegate,UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
circles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCellIdentifier, for: indexPath) as? CircleCollectionViewCell else {
fatalError("Bad instance of FavoritesCollectionViewCell")
}
cell.nameLabel.text = circles[indexPath.row].name
return cell
}
func setTableViewDelegates(){
circleCollectionView.delegate = self
circleCollectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(circles.count)
}
}
extension FlowViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = cellWidth
let height = cellHeight
return CGSize(width: width, height: height)
}
}
class myCarouselFlowLayout: UICollectionViewFlowLayout,UICollectionViewDelegateFlowLayout {
private let fadeFactor: CGFloat = 0.5
override var collectionViewContentSize: CGSize {
super.collectionViewContentSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
super.layoutAttributesForElements(in: rect)?.map {
self.layoutAttributesForItem(at: $0.indexPath)!
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let superValue = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil }
guard let cv = collectionView else { return nil }
let collectionMidPoint = CGPoint(x: cv.bounds.midX, y: cv.bounds.midY)
let itemMidPoint = superValue.center
let distance = abs(itemMidPoint.x - collectionMidPoint.x)
if distance > 100 {
UIView.animate(withDuration: 0.2) {
superValue.alpha = self.fadeFactor
superValue.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
cv.layoutIfNeeded()
}
}
return superValue
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
true
}
var velocityThresholdPerPage: CGFloat = 2
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
let pageLength: CGFloat
let approxPage: CGFloat
let currentPage: CGFloat
pageLength = (self.itemSize.width + self.minimumLineSpacing)
approxPage = collectionView.contentOffset.x / pageLength
currentPage = round(approxPage)
if velocity.x == 0 {
return CGPoint(x: currentPage * pageLength, y: 0)
}
var nextPage: CGFloat = currentPage + (velocity.x > 0 ? 1 : -1)
let increment = velocity.x / velocityThresholdPerPage
nextPage += round(increment)
return CGPoint(x: nextPage * pageLength, y: 0)
}
Please give me some advice on where and how should I use animations on it? Should I do it with UIView.animate or something else? I know that I need to use layoutIfNeeded() to animate constraints, but it doesn't work with it.

How to use ScrollToItem(at:) when using a custom collectionView layout to alter cell sizes

I have a custom layout for a collectionView. This custom layout increases the width of the center cell. Here is the custom layout class that does this. Look at the shiftedAttributes function to see how its done
class CustomCollectionViewLayout: UICollectionViewLayout {
private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
private var contentWidth = CGFloat()
private var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
private var oldBounds = CGRect.zero
private var cellWidth: CGFloat = 5
private var collectionViewStartY: CGFloat {
guard let collectionView = collectionView else {
return 0
}
return collectionView.bounds.minY
}
private var collectionViewHeight: CGFloat {
return collectionView!.frame.height
}
override public var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: collectionViewHeight)
}
override public func prepare() {
print("calling prepare")
guard let collectionView = collectionView,
cache.isEmpty else {
return
}
updateInsets()
collectionView.decelerationRate = .fast
cache.removeAll(keepingCapacity: true)
cache = [IndexPath: UICollectionViewLayoutAttributes]()
oldBounds = collectionView.bounds
var xOffset: CGFloat = 0
var cellWidth: CGFloat = 5
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let cellIndexPath = IndexPath(item: item, section: 0)
let cellattributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
cellattributes.frame = CGRect(x: xOffset, y: 0, width: cellWidth, height: collectionViewHeight)
xOffset = xOffset + cellWidth
contentWidth = max(contentWidth,xOffset)
cache[cellIndexPath] = cellattributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
visibleLayoutAttributes.removeAll(keepingCapacity: true)
for (_, attributes) in cache {
visibleLayoutAttributes.append(self.shiftedAttributes(from: attributes))
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = cache[indexPath] else { fatalError("No attributes cached") }
return shiftedAttributes(from: attributes)
}
override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if oldBounds.size != newBounds.size {
cache.removeAll(keepingCapacity: true)
}
return true
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
if context.invalidateDataSourceCounts { cache.removeAll(keepingCapacity: true) }
super.invalidateLayout(with: context)
}
}
extension CustomCollectionViewLayout {
func updateInsets() {
guard let collectionView = collectionView else { return }
collectionView.contentInset.left = (collectionView.bounds.size.width - cellWidth) / 2
collectionView.contentInset.right = (collectionView.bounds.size.width - cellWidth) / 2
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let midX: CGFloat = collectionView.bounds.size.width / 2
guard let closestAttribute = findClosestAttributes(toXPosition: proposedContentOffset.x + midX) else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
return CGPoint(x: closestAttribute.center.x - midX, y: proposedContentOffset.y)
}
private func findClosestAttributes(toXPosition xPosition: CGFloat) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else { return nil }
let searchRect = CGRect(
x: xPosition - collectionView.bounds.width, y: collectionView.bounds.minY,
width: collectionView.bounds.width * 2, height: collectionView.bounds.height
)
let closestAttributes = layoutAttributesForElements(in: searchRect)?.min(by: { abs($0.center.x - xPosition) < abs($1.center.x - xPosition) })
return closestAttributes
}
private var continuousFocusedIndex: CGFloat {
guard let collectionView = collectionView else { return 0 }
let offset = collectionView.bounds.width / 2 + collectionView.contentOffset.x - cellWidth / 2
return offset / cellWidth
}
private func shiftedAttributes(from attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let attributes = attributes.copy() as? UICollectionViewLayoutAttributes else { fatalError("Couldn't copy attributes") }
let roundedFocusedIndex = round(continuousFocusedIndex)
let focusedItemWidth = CGFloat(20)
if attributes.indexPath.item == Int(roundedFocusedIndex){
attributes.transform = CGAffineTransform(scaleX: 10, y: 1)
} else {
let translationDirection: CGFloat = attributes.indexPath.item < Int(roundedFocusedIndex) ? -1 : 1
attributes.transform = CGAffineTransform(translationX: translationDirection * 20, y: 0)
}
return attributes
}
}
Here is the View Controller which contains the collectionView that uses this layout:
class ViewController: UIViewController, UICollectionViewDelegate,UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: "singleCell", for: indexPath)
cell.backgroundColor = UIColor.random()
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
#IBOutlet weak var picker: UIPickerView!
#IBOutlet weak var customCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
customCollectionView.delegate = self
customCollectionView.dataSource = self
picker.delegate = self
picker.dataSource = self
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func goTo(_ sender: Any) {
let indexPath = IndexPath(item: picker.selectedRow(inComponent: 0), section: 0)
customCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 10
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return String(row)
}
}
Note the pickerView where you can pick an index that is used in the goTo button to scrollTo the item at that index. Here it is in action:
See how even though I am staying on the same index, it keeps scrolling around, and doesn't really scroll to that index anyway. When I don't shift the attributes (with shiftedAttributes) and just return them normally in the custom layout, the scrollTo works fine.
So it seems something about the placement of each cell is used when doing scrollToItem(at:) which is getting confused by the shifted attributes? How do I scroll to a particular index when the sizes of the cells are subject to change?
EDIT: here is the entire project code if you wanna try it yourself:
It appears that scrollToItem() is using a fixed layout size.
I think you will have to calculate the offset manually and use setContentOffset()
//To do: Calculate widths of cells up to the cell you want to scroll to
var calculatedOffset: CGFloat
//Then scroll to the offset calculated
customCollectionView.setContentOffset(CGPoint(x: calculatedOffset, y: 0.0), animated: true)

the UICollectionView cell is centered why?

I had a problem with the collectionView.
In fact, I had this algorithm :
- the first cell's width is screen_width/2
- the second cell's width is : screen_width
- the third cell's width is screen_width/2
- the fourth cell's width is : screen_width.
and so on.
This is my code in the method
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath:
userStory.typePhotoStory = ( (indexPath.row + 1) % 3) == 0 ? #"landscape" : ((indexPath.row + 1) % 3) == 1 ? #"portrait": #"landscape";
if ([userStory.typePhotoStory isEqualToString:#"portrait"]) {
cellSizeFinalWidth = cellSize / 2 ;
cellSizeFinalHeight = cellSizeFinalWidth + 20 ;
} else {
cellSizeFinalWidth = cellSize + padding ;
cellSizeFinalHeight = cellSize / 2 + 20 ;
}
}
But I see that, The first and the third cell are placed in the center of the screen , not started from the left.
Please help me on this issue?
You can directly use this code,
Create a new file and paste this below code for custom layout.
import UIKit
protocol NewLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, SizeOfCellAtIndexPath indexPath:IndexPath) -> CGSize
}
class NewFlowLayout: UICollectionViewFlowLayout {
weak var delegate : NewLayoutDelegate?
fileprivate var numberOfColumns = 1
fileprivate var cellPadding: CGFloat = 6
fileprivate var cache = [UICollectionViewLayoutAttributes]()
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() {
super.prepare()
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let xOffset : CGFloat = 0
let column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let aSize = delegate?.collectionView(collectionView, SizeOfCellAtIndexPath: indexPath)
let height = cellPadding * 2 + (aSize?.height)!
let frame = CGRect(x: xOffset, y: yOffset[column], width: (aSize?.width)!, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
}
}
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]
}
}
In your controller file
import UIKit
class NewCell: UICollectionViewCell {
}
class NewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, NewLayoutDelegate {
#IBOutlet weak var myCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.collectionViewLayout = NewFlowLayout()
if let layout = myCollectionView.collectionViewLayout as? NewFlowLayout {
layout.delegate = self
}
}
// Do size logic here
func collectionView(_ collectionView: UICollectionView, SizeOfCellAtIndexPath indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
if indexPath.item % 2 == 0 {
return CGSize(width: (collectionView.frame.size.width - (insets.left + insets.right))/2,
height: 60)
}
return CGSize(width: collectionView.frame.size.width - (insets.left + insets.right),
height: 60)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NewCell", for: indexPath) as! NewCell
return cell
}
}
Output

CollectionView Flowlayout Customize

I am making a profile picture collectionview like tinder edit profile pictures. I want first cell bigger than others and 2, 3 cells besides first cell and others should like 3, 4, 5.
Any suggestion?
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return CGSize(width: 213.34, height: 213.34)
} else {
return CGSize(width: 101.66, height:101.66 )
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let lbl = cell.viewWithTag(1) as! UILabel
lbl.text = String(format: "%d", indexPath.row + 1)
return cell
}
}
You need to implement an UICollectionViewLayout, I had called it FillingLayout, Note that you can adjust the number of columns and the size of your big cells with the delegate methods
Explanation
You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = [] and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.
Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.
here is the full code
import UIKit
protocol FillingLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
// Returns the amount of columns that have to display at that moment
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
}
class FillingLayout: UICollectionViewLayout {
weak var delegate: FillingLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
private var columsHeights : [CGFloat] = []
private var avaiableSpaces : [(Int,CGFloat)] = []
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
var columnsQuantity : Int{
get{
if(self.delegate != nil)
{
return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
}
return 0
}
}
//MARK: PRIVATE METHODS
private func shortestColumnIndex() -> Int{
var retVal : Int = 0
var shortestValue = MAXFLOAT
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) < shortestValue)
{
shortestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
//MARK: PRIVATE METHODS
private func largestColumnIndex() -> Int{
var retVal : Int = 0
var largestValue : Float = 0.0
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) > largestValue)
{
largestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
private func canUseBigColumnOnIndex(columnIndex:Int,size:Int) ->Bool
{
if(columnIndex < self.columnsQuantity - (size-1))
{
let firstColumnHeight = columsHeights[columnIndex]
for i in columnIndex..<columnIndex + size{
if(firstColumnHeight != columsHeights[i]) {
return false
}
}
return true
}
return false
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// Set all column heights to 0
self.columsHeights = []
for _ in 0..<self.columnsQuantity {
self.columsHeights.append(0)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: Int = delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath)
let blockWidth = (contentWidth/CGFloat(columnsQuantity))
let width = blockWidth * CGFloat(viewSize)
let height = width
var columIndex = self.shortestColumnIndex()
var xOffset = (contentWidth/CGFloat(columnsQuantity)) * CGFloat(columIndex)
var yOffset = self.columsHeights[columIndex]
if(viewSize > 1){//Big Cell
if(!self.canUseBigColumnOnIndex(columnIndex: columIndex,size: viewSize)){
// Set column height
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
self.avaiableSpaces.append((i,yOffset))
self.columsHeights[i] += blockWidth
}
}
// Set column height
yOffset = columsHeights[largestColumnIndex()]
xOffset = 0
columIndex = 0
}
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
//current height
let currValue = self.columsHeights[i]
//new column height with the update
let newValue = yOffset + height
//space that will remaing in blank, this must be 0 if its ok
let remainder = (newValue - currValue) - CGFloat(viewSize) * blockWidth
if(remainder > 0) {
debugPrint("Its bigger remainder is \(remainder)")
//number of spaces to fill
let spacesTofillInColumn = Int(remainder/blockWidth)
//we need to add those spaces as avaiableSpaces
for j in 0..<spacesTofillInColumn {
self.avaiableSpaces.append((i,currValue + (CGFloat(j)*blockWidth)))
}
}
self.columsHeights[i] = yOffset + height
}
}
}else{
//if there is not avaiable space
if(self.avaiableSpaces.count == 0)
{
// Set column height
self.columsHeights[columIndex] += height
}else{//if there is some avaiable space
yOffset = self.avaiableSpaces.first!.1
xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
self.avaiableSpaces.remove(at: 0)
}
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
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]
}
}
UPDATED
You need to setup your viewController as FillingLayoutDelegate
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view.
if let layout = self.collectionView.collectionViewLayout as? FillingLayout
{
layout.delegate = self
}
}
FillingLayoutDelegate implementation in your ViewController
extension ViewController: FillingLayoutDelegate{
func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
if(indexPath.row == 0 || indexPath.row == 4)
{
return 2
}
if(indexPath.row == 5)
{
return 3
}
return 1
}
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
return 3
}
}
ScreenShot working
You can use UICollectionViewLayout to handle this. Code is given below:
UICollectionViewLayout class to define layout:
class CustomCircularCollectionViewLayout: UICollectionViewLayout {
var itemSize = CGSize(width: 200, height: 150)
var attributesList = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
let itemNo = collectionView?.numberOfItems(inSection: 0) ?? 0
let length = (collectionView!.frame.width - 40)/3
itemSize = CGSize(width: length, height: length)
attributesList = (0..<itemNo).map { (i) -> UICollectionViewLayoutAttributes in
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
var x = CGFloat(i%3)*(itemSize.width+10) + 10
var y = CGFloat(i/3)*(itemSize.width+10) + 10
if i > 2 {
y += (itemSize.width+10)
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
} else if i == 0 {
attributes.frame = CGRect(x: x, y: y, width: itemSize.width*2+10, height: itemSize.height*2+10)
} else {
x = itemSize.width*2 + 30
if i == 2 {
y += itemSize.height + 10
}
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
}
return attributes
}
}
override var collectionViewContentSize : CGSize {
return CGSize(width: collectionView!.bounds.width, height: (itemSize.height + 10)*CGFloat(ceil(Double(collectionView!.numberOfItems(inSection: 0))/3))+(itemSize.height + 20))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.row < attributesList.count
{
return attributesList[indexPath.row]
}
return nil
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
Now set this class as your collection view layout:
self.collectionView!.collectionViewLayout = circularLayoutObject
This will display your grid as given below:
Let me know if it works for you.

UICollectionViewCell's height using custom layout

I have ran into a problem during UICollectionView custom layout implementation.
The thing is that I need to calculate collection view cell's height in custom layout's prepare(). To do so I have:
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat
method in custom layout's delegate protocol which is implemented in my view controller.
However this method is called before cell is dequeued and actually has any data based on which I could calculate it's height. Thus if cell's content exceeds it's initial bounds - I can't see part of content.
Has anyone encountered the same problem with custom layout? How did you solve it?
Protocol for custom layout:
protocol CustomLayoutDelegateProtocol: class {
func numberOfSectionsInRow() -> Int
func indecesOfSectionsInRow() -> [Int]
func minimumInteritemSpace() -> CGFloat
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
}
Custom layout itself:
class CustomLayoutClass: UICollectionViewLayout {
weak var delegate: CustomLayoutDelegateProtocol? {
didSet {
setupLayout()
}
}
private var cache = [UICollectionViewLayoutAttributes]()
private var contentHeight: CGFloat = 0.0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else { return 0 }
return collectionView.bounds.width
}
private var interitemSpace: CGFloat?
private var numberOfColumns: Int?
private var columnedSections: [Int]?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupLayout() {
numberOfColumns = delegate?.numberOfSectionsInRow()
columnedSections = delegate?.indecesOfSectionsInRow()
interitemSpace = delegate?.minimumInteritemSpace()
}
override func invalidateLayout() {
cache.removeAll()
super.invalidateLayout()
}
override func prepare() {
if cache.isEmpty {
guard let collectionView = collectionView,
let numberOfColumns = numberOfColumns,
let columnedSections = columnedSections,
let interitemSpace = interitemSpace else { return }
let columnWidth = (contentWidth / CGFloat(numberOfColumns))
var xOffset = [CGFloat]()
for column in 0..<numberOfColumns {
var interitemSpace = interitemSpace
if column == 0 { interitemSpace = 0 }
xOffset.append(CGFloat(column) * columnWidth + interitemSpace)
}
var yOffset: CGFloat = 0.0
for section in 0..<collectionView.numberOfSections {
for item in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(item: item, section: section)
guard let sectionInsets = delegate?.collectionView(collectionView, layout: self, insetForSectionAt: indexPath.section),
let height = delegate?.heightForItem(collectionView, at: indexPath) else { continue }
let width = columnedSections.contains(section) ? columnWidth : contentWidth
let xOffsetIdx = columnedSections.contains(section) ? columnedSections.index(of: section)! % numberOfColumns : 0
yOffset += sectionInsets.top
let frame = CGRect(x: xOffset[xOffsetIdx], y: yOffset, width: width, height: height)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
let isLastInRow = (columnedSections.contains(section) && columnedSections.index(of: section)! % numberOfColumns == (numberOfColumns-1))
let isNotColumnedSection = !columnedSections.contains(section)
if isLastInRow || isNotColumnedSection {
yOffset += height + sectionInsets.bottom
}
}
}
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
And implementation for protocol (from view controller):
extension ViewController: CustomLayoutDelegateProtocol {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
guard let sectionType = InvoiceSectionIndexType(rawValue: section) else { return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
switch sectionType {
case .receiver:
return UIEdgeInsets(top: Constants.bigLineSpace, left: 0, bottom: Constants.bigLineSpace, right: 0)
default:
return UIEdgeInsets(top: 0, left: 0, bottom: Constants.commonLineSpace, right: 0)
}
}
func numberOfSectionsInRow() -> Int {
return Constants.numberOfSectionsInRow
}
func indecesOfSectionsInRow() -> [Int] {
return [0, 1]
}
func minimumInteritemSpace() -> CGFloat {
return Constants.interitemSpace
}
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat {
guard let sectionType = InvoiceSectionIndexType(rawValue: indexPath.section) else { return 0 }
switch sectionType {
case .next:
return Constants.nextButtonHeight
default:
return Constants.estimatedRowHeight
}
}
}
I resolved this issue on my own and the solution is quite simple though not very obvious.
Since we have indexPath for concrete cell we can find our content (in my case content is simple text which is put into a label). Then we can create label (which we will not display since it's needed only for calculations) with width (in my case I know width for cells) and height which is CGFloat.greatestFiniteMagnitude. We then apply sizeToFit() to our label and now we have label with appropriate height. The only thing we should do now - apply this new height to our cell.
Sample code:
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
// ask your DataSource for piece of data with indexPath
let testLabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
testLabel.text = // your text
testLabel.sizeToFit()
if testLabel.bounds.height > Constants.defaultContentLabelHeight {
return Constants.estimatedRowHeight + (testLabel.bounds.height - Constants.defaultContentLabelHeight)
} else {
return Constants.estimatedRowHeight
}
}

Resources