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?
-
1try searching for gradient effect This may help http://stackoverflow.com/questions/22726103/ios-uitableview-fade-bottom-cell-and-top-cell-as-you-scroll – Saheb Singh Jun 21 '16 at 10:21
-
i like the idea of using a gradient mask however I believe that will effect all of the contents of the scrollView and not just the right cells. – alionthego Jun 21 '16 at 10:24
-
you fading top bottom or both? – agibson007 Mar 09 '17 at 20:47
3 Answers
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.

- 4,173
- 2
- 19
- 24
-
Is it possible to fade only when user start scrolling? Also, when the user gets to the end of the scrollView the bottom cell to be without fade? – Adrian Macarenco Oct 09 '20 at 10:37
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
}
}

- 3,423
- 2
- 14
- 26
-
Thanks! The key bit I was missing was returning true for shouldInvalidateLayout – Ric Santos Mar 14 '17 at 00:57
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
}
}
}
}

- 59,527
- 19
- 156
- 165