Collectionview dynamic width not worked Swift - ios

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

Related

How to get all UICollectionViewCells to share the same height?

I have a UICollectionView where the Cell's do not fill their vertical space. What adjustment should I make to ensure each cell fills up the entire cell area? Or at least all share the same height for the row they are on?
Here is the Storyboard
UICollectionViewFlowLayout
class AddServiceFlowLayout: UICollectionViewFlowLayout {
let cellsPerRow: Int
init(cellsPerRow: Int, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
self.cellsPerRow = cellsPerRow
super.init()
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
itemSize = CGSize(width: itemWidth, height: itemWidth)
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
UICollectionViewCell
class AddServiceViewCell: UICollectionViewCell {
#IBOutlet weak var labelStackView: UIStackView!
#IBOutlet weak var tvServiceName: UILabel!
#IBOutlet weak var tvQuantityNeeded: UILabel!
public var onCellTapped: (() -> ())?
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = 10.0
}
override func prepareForReuse() {
super.prepareForReuse()
self.tvServiceName.text = nil
self.tvQuantityNeeded.show()
}
public static func nib() -> UINib {
return UINib.init(nibName: identifier, bundle: Bundle(for: ServiceLineItemCell.self))
}
public func configure(with service: TowService){
self.tvServiceName.text = service.description
if(service.calculated){
self.tvQuantityNeeded.hide()
}
}
public func eventTriggered()
{
onCellTapped?()
}
}
UIViewController
class AddServiceViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let columnLayout = AddServiceFlowLayout(
cellsPerRow: 3,
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Add Service"
// Register cell classes
self.collectionView!.delegate = self
self.collectionView!.dataSource = self
self.collectionView!.collectionViewLayout = columnLayout
self.collectionView!.contentInsetAdjustmentBehavior = .always
self.collectionView!.register(AddServiceViewCell.nib().self, forCellWithReuseIdentifier: AddServiceViewCell.identifier)
}
// removed for brevity ....
// MARK: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
extension AddServiceViewController : UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allServices.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddServiceViewCell.identifier, for: indexPath) as! AddServiceViewCell
cell.configure(with: allServices[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.onConfirmAdd(allServices[indexPath.row])
}
}
Here is a road map
1- You need to implement sizeForItemAt
2- Consider you have 3 columns per row , create a function that that accepts 3 Items ( introduced with the current indexPath.item ) from your model and manually calculate maximum height for each string in that model
3- Return the maximum height and by this you will set that height for all cells of same row

UICollection View "Reload Data" After Scrolling Crash

I have made a collection view with cells arranged in rows in columns using a great tutorial. I have added a button to a toolbar in the main view controller that calls collectionView.reloadData() as I want a user to be able to edit values which will in turn update the datasource and then reload the collection view to show the updates.
Running this on a simulator it works, but if any scrolling takes place it causes this crash *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'. If no scrolling has taken place then calling collectionView.reloadData() works. I can't find where this empty array is which is causing the crash. I have tried printing all the arrays that are used in the code in the console but none appear to be empty. Have tried commenting out various lines of code to try and narrow down where the problem is, it seems to be something in the override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? block. I have also tried reseting the collection view frame co-ordinates to 0 before reload data is called but that also didn't work. Have been stuck going round in circles for a few days which no luck. Any suggestions as to where I am going wrong would be hugely appreciated! My code so far is below (please excuse the long winded explanation and code);
View Controller
import UIKit
class ViewController: UIViewController {
// variable to contain cell indexpaths sent from collectionViewFlowLayout.
var cellIndexPaths = [IndexPath] ()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout! {
didSet {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
#IBAction func editButtonPressed(_ sender: UIButton) {
collectionView.reloadData()
}
#IBAction func doneButtonPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension ViewController:UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! collectionViewCell
cell.backgroundColor = .green
cell.textLabel.text = "\(indexPath) - AAAABBBBCCCC"
return cell
}
}
extension ViewController:UICollectionViewDelegate, IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>) {
cellIndexPaths = indexPathArray.uniqueValues
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var cellArray = [UICollectionViewCell] ()
print(cellIndexPaths)
for indexPathItem in cellIndexPaths {
if let cell = collectionView.cellForItem(at: indexPathItem) {
if indexPathItem.section == indexPath.section {
cellArray.append(cell)
}
}
for cells in cellArray {
cells.backgroundColor = .red
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { (timer) in
cells.backgroundColor = .green
}
}
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.cellForItem(at: indexPath)
if let safeCell = cell {
let cellSize = CGSize(width: safeCell.frame.width, height: safeCell.frame.height)
return cellSize
} else {
return CGSize (width: 300, height: 100)
}
}
}
extension Array where Element: Hashable {
var uniqueValues: [Element] {
var allowed = Set(self)
return compactMap { allowed.remove($0) }
}
}
Flow Layout
import UIKit
protocol IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>)
}
class collectionViewFlowLayout: UICollectionViewFlowLayout {
override var collectionViewContentSize: CGSize {
return CGSize(width: 10000000, height: 100000)
}
override func prepare() {
setupAttributes()
indexItemDelegate()
}
// MARK: - ATTRIBUTES FOR ALL CELLS
private var allCellAttributes: [[UICollectionViewLayoutAttributes]] = []
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for rowAttrs in allCellAttributes {
for itemAttrs in rowAttrs where rect.intersects(itemAttrs.frame) {
layoutAttributes.append(itemAttrs)
}
}
return layoutAttributes
}
// MARK: - SETUP ATTRIBUTES
var cellIndexPaths = [IndexPath] ()
private func setupAttributes() {
allCellAttributes = []
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for row in 0..<rowsCount {
var rowAttrs: [UICollectionViewLayoutAttributes] = []
xOffset = 0
for col in 0..<columnsCount(in: row) {
let itemSize = size(forRow: row, column: col)
let indexPath = IndexPath(row: row, column: col)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
rowAttrs.append(attributes)
xOffset += itemSize.width
cellIndexPaths.append(indexPath)
}
yOffset += rowAttrs.last?.frame.height ?? 0.0
allCellAttributes.append(rowAttrs)
}
}
// MARK: - CONVERT SECTIONS TO ROWS, ITEMS TO COLUMNS
private var rowsCount: Int {
return collectionView!.numberOfSections
}
private func columnsCount(in row: Int) -> Int {
return collectionView!.numberOfItems(inSection: row)
}
// MARK: - GET CELL SIZE
private func size(forRow row: Int, column: Int) -> CGSize {
guard let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout,
let size = delegate.collectionView?(collectionView!, layout: self, sizeForItemAt: IndexPath(row: row, column: column)) else {
assertionFailure("Implement collectionView(_,layout:,sizeForItemAt: in UICollectionViewDelegateFlowLayout")
return .zero
}
return size
}
private func indexItemDelegate () {
let delegate = collectionView?.delegate as? IndexPathDelegate
delegate?.getIndexPaths(indexPathArray: cellIndexPaths)
}
}
// MARK: - INDEX PATH EXTENSION
//creates index path with rows and columns instead of sections and items
private extension IndexPath {
init(row: Int, column: Int) {
self = IndexPath(item: column, section: row)
}
}
Collection Cell
import UIKit
class collectionViewCell: UICollectionViewCell {
#IBOutlet weak var textLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
I have managed to get around the issue by using the UICollectionView.reloadSections(sections: IndexSet) method. This doesn't cause any crashes. I loop through all sections and add each section to an IndexSet variable then use that in the reload sections method like this;
var indexSet = IndexSet()
let rowCount = collectionView.numberOfSections
for row in 0..<rowCount {
indexSet = [row]
collectionView.reloadSections(indexSet)
}

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.

Changing UICollectionview cell XY position - swift 3

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
}

How can I make a particular cell of an iOS collectionView fade out as the collectionView scrolls?

I want to make all the right side cells of my UICollectionView fade out as they scroll similar to Apple's messages app but not effect the color or transparency of the other cells in the collectionView. Is there a way to adjust the transparency of a UICollectionViewCell based on it's scroll position to achieve that effect?
You can do a lot of fun stuff to collection views. I like to subclass UICollectionViewFlowLayout. Here is an example that fades the top and the bottom of the collection view based on distance from center. I could modify it to fade only the very edges but you should figure it after you look through the code.
import UIKit
class FadingLayout: UICollectionViewFlowLayout,UICollectionViewDelegateFlowLayout {
//should be 0<fade<1
private let fadeFactor: CGFloat = 0.5
private let cellHeight : CGFloat = 60.0
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init(scrollDirection:UICollectionViewScrollDirection) {
super.init()
self.scrollDirection = scrollDirection
}
override func prepare() {
setupLayout()
super.prepare()
}
func setupLayout() {
self.itemSize = CGSize(width: self.collectionView!.bounds.size.width,height:cellHeight)
self.minimumLineSpacing = 0
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func scrollDirectionOver() -> UICollectionViewScrollDirection {
return UICollectionViewScrollDirection.vertical
}
//this will fade both top and bottom but can be adjusted
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesSuper: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) as [UICollectionViewLayoutAttributes]!
if let attributes = NSArray(array: attributesSuper, copyItems: true) as? [UICollectionViewLayoutAttributes]{
var visibleRect = CGRect()
visibleRect.origin = collectionView!.contentOffset
visibleRect.size = collectionView!.bounds.size
for attrs in attributes {
if attrs.frame.intersects(rect) {
let distance = visibleRect.midY - attrs.center.y
let normalizedDistance = abs(distance) / (visibleRect.height * fadeFactor)
let fade = 1 - normalizedDistance
attrs.alpha = fade
}
}
return attributes
}else{
return nil
}
}
//appear and disappear at 0
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: itemIndexPath)! as UICollectionViewLayoutAttributes
attributes.alpha = 0
return attributes
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: itemIndexPath)! as UICollectionViewLayoutAttributes
attributes.alpha = 0
return attributes
}
}
And in your setup in your controller with the collection view it would look like this.
let layout = FadingLayout(scrollDirection: .vertical)
collectionView.delegate = self
collectionView.dataSource = self
self.collectionView.setCollectionViewLayout(layout, animated: false)
I can tell you how to modify it if I knew the use case a bit better.
This is quite simple if you subclass UICollectionViewFlowLayout. First thing you'll need to do is make sure the visible attributes are recalculated when bounds change/scroll happens by returning true in
shouldInvalidateLayout(forBoundsChange newBounds: CGRect)
Then in layoutAttributesForElements(in rect: CGRect) delegate call, get the attributes calculated by the super class and modify the alpha value based on the offset of the item in the visible bounds, thats it.
Distinguishing between left/right side items can be handled in the controller with whatever logic you have and communicated to the layout class to avoid applying this effect on left side items. (I used ´CustomLayoutDelegate´ for that which is implemented in the controller that simply identifies items with odd indexPath.row as left side cells)
Here is a demo that applies this effect on items with with even indexPath.row skipping odd rows
import UIKit
class ViewController: UIViewController {
/// Custom flow layout
lazy var layout: CustomFlowLayout = {
let l: CustomFlowLayout = CustomFlowLayout()
l.itemSize = CGSize(width: self.view.bounds.width / 1.5, height: 100)
l.delegate = self
return l
}()
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.lightGray
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
}
}
extension ViewController: UICollectionViewDataSource, CustomLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 30
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = UIColor.black
return cell
}
// MARK: CustomLayoutDelegate
func cellSide(forIndexPath indexPath: IndexPath) -> CellSide {
// TODO: Your implementation to distinguish left/right indexPath
// Even rows are .right and Odds .left
if indexPath.row % 2 == 0 {
return .right
} else {
return .left
}
}
}
public enum CellSide {
case right
case left
}
protocol CustomLayoutDelegate: class {
func cellSide(forIndexPath indexPath: IndexPath) -> CellSide
}
class CustomFlowLayout: UICollectionViewFlowLayout {
/// Delegates distinguishing between left and right items
weak var delegate: CustomLayoutDelegate!
/// Maximum alpha value
let kMaxAlpha: CGFloat = 1
/// Minimum alpha value. The alpha value you want the first visible item to have
let kMinAlpha: CGFloat = 0.3
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let cv = collectionView, let rectAtts = super.layoutAttributesForElements(in: rect) else { return nil }
for atts in rectAtts {
// Skip left sides
if delegate.cellSide(forIndexPath: atts.indexPath) == .left {
continue
}
// Offset Y on visible bounds. you can use
// ´cv.bounds.height - (atts.frame.origin.y - cv.contentOffset.y)´
// To reverse the effect
let offset_y = (atts.frame.origin.y - cv.contentOffset.y)
let alpha = offset_y * kMaxAlpha / cv.bounds.height
atts.alpha = alpha + kMinAlpha
}
return rectAtts
}
// Invalidate layout when scroll happens. Otherwise atts won't be recalculated
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Sure! Note that UICollectionView is a subclass of UIScrollView, and that your UICollectionViewController is already the delegate of the collection view. This means that it also conforms to the UIScrollViewDelegate protocol, which includes a bunch of methods to inform you about scroll position changes.
Most notable to me is scrollViewDidScroll(_:), which will be called when the contentOffset in the collection view changes. You might implement that method to iterate over the collection view's visibleCells, either adjusting the cell's alpha yourself or sending some message to the cell to notify it to adjust its own alpha based on its frame and offset.
The simplest possible implementation I could come up with that does this – respecting your right-side-only requirement – is as follows. Note that this might exhibit some glitches near the top or the bottom of the view, since the cell's alpha is only adjusted on scroll, not on initial dequeue or reuse.
class FadingCollectionViewController: UICollectionViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let collectionView = collectionView else {
return
}
let offset = collectionView.contentOffset.y
let height = collectionView.frame.size.height
let width = collectionView.frame.size.width
for cell in collectionView.visibleCells {
let left = cell.frame.origin.x
if left >= width / 2 {
let top = cell.frame.origin.y
let alpha = (top - offset) / height
cell.alpha = alpha
} else {
cell.alpha = 1
}
}
}
}

Resources