How to create a Stretchy Header for UICollectionViewCompositionalLayout with multiple sections? - ios

This is my first time implementing UICollectionViewCompositionalLayout and im confused on how to implement a Stretchy Header here. Right now I have just modified the dataSource.supplementaryViewProvider function to include my custom HeaderView but I don't know how to make it attach to top.
I did find some code for other types of collectionView layouts but those don't work with UICollectionViewCompositionalLayout. For other layouts I found that I need to override this override func layoutAttributesForElements(in rect: CGRect) but where and how? I would like to know a method from the scratch. Below is my method which does not work at all with UICollectionViewCompositionalLayout.
This is how im creating my Header:
class StretchyCollectionHeaderView: UICollectionReusableView {
static let reuseIdentifier = "stretchyCollectionHeaderView-reuse-identifier"
let imageView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "HeaderHomePage"))
iv.contentMode = .scaleAspectFill
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
// custom code for layout
backgroundColor = .red
addSubview(imageView)
imageView.fillSuperview()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Im using UICollectionReusableView instead of UIView because all section headers are passed as UICollectionReusableView. Im using an extension which will connect imageView's bottom to header view and for other constraints, I did not include that because I think it isn't even used which I will come back to at the end of explanation.
This is my Layout for CollectionViewLayout with Stretchy Header:
class StretchyHeaderLayout: UICollectionViewCompositionalLayout {
// we want to modify the attributes of our header component somehow
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
layoutAttributes?.forEach({ (attributes) in
if attributes.representedElementKind == UICollectionView.elementKindSectionHeader && attributes.indexPath.section == 0 {
guard let collectionView = collectionView else { return }
let contentOffsetY = collectionView.contentOffset.y
print(contentOffsetY)
if contentOffsetY > 0 {
return
}
let width = collectionView.frame.width
let height = attributes.frame.height - contentOffsetY
// header
attributes.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
}
})
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Then I simply register all cells and supplementary views. Then im setting up my collectionViewLayout like this:
func generateLayout() -> UICollectionViewLayout {
let layout = StretchyHeaderLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let isWideView = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
let sectionLayoutKind = Section.allCases[sectionIndex]
switch (sectionLayoutKind) {
case .locationTab: return self.generateLocationLayout(isWide: isWideView)
case .selectCategory: return self.generateCategoriesLayout()
case .valueAddedServices: return self.generatValueAddedServicesLayout(isWide: isWideView)
}
}
return layout
}
im setting layout = StretchyHeaderLayout as this was the only possible way I could think of adding Stretchy Header layout.
And finally this is how im setting up my section headers:
dataSource.supplementaryViewProvider = { (
collectionView: UICollectionView,
kind: String,
indexPath: IndexPath) -> UICollectionReusableView? in
if indexPath.section == 0 {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier,
for: indexPath) as? StretchyCollectionHeaderView else { fatalError("Cannot create header view") }
supplementaryView.imageView.image = UIImage(named: "HeaderHomePage")
return supplementaryView
}
else {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
supplementaryView.label.text = Section.allCases[indexPath.section].rawValue
return supplementaryView
}
}
Here im just using StretchyCollectionHeaderView for first section and for others im using another HeaderView which just contains a label.
What I think is happening is because of above function, its just setting header view with StreatchyCollectionHeaderView for first section but not accessing any code inside StretchyHeaderLayout and thus not sticking to top.
Unlike UITableView, we cannot attach a CollectionView header instead of adding section header or in my case section header with image for first section and attach to top?
How to create a stretchy header for UICollectionViewCompositionalLayout properly?

I found an answer by modifying some code I found for StretchyTableHeaderView. I will try to explain what I did in short before adding the code below. So first I simply created CollectionViewReusableView like for any SupplementaryView you create for CollectionViews. Actually I just found this code for stretchy TableViewHeader, I just converted it to UICollectionReusableView. But this header view contains a scrollViewDidScroll function which manipulates the bottom constraints for your containerView and imageView and also manipulates the height of imageView so it increases with offset of the scrollView.
After that you just register this to your collectionView, pass this as supplementaryView for First Section and give it a height while creating the layout. And finally in scrollViewDidScroll delegate method, look for your supplementary view and if found, just call the scrollViewDidScroll function inside your header.
This is the StretchHeaderCollectionResulableView class:
final class StretchyCollectionHeaderView: UICollectionReusableView {
static let reuseIdentifier = "stretchy-homePage-header-view-reuse-identifier"
public let imageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private var imageViewHeight = NSLayoutConstraint()
private var imageViewBottom = NSLayoutConstraint()
private var containerView = UIView()
private var containerViewHeight = NSLayoutConstraint()
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
setViewConstraints()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/// Create Subviews
private func createViews() {
addSubview(containerView)
containerView.addSubview(imageView)
}
/// Setup View Constraints
func setViewConstraints() {
NSLayoutConstraint.activate([
widthAnchor.constraint(equalTo: containerView.widthAnchor),
centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = translatesAutoresizingMaskIntoConstraints
containerViewHeight = containerView.heightAnchor.constraint(equalTo: self.heightAnchor)
containerViewHeight.isActive = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewBottom = imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
imageViewBottom.isActive = true
imageViewHeight = imageView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
imageViewHeight.isActive = true
}
/// Notify View of scroll change from container
public func scrollviewDidScroll(scrollView: UIScrollView) {
containerViewHeight.constant = scrollView.contentInset.top
let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
containerView.clipsToBounds = offsetY <= 0
imageViewBottom.constant = offsetY >= 0 ? 0 : -offsetY / 2
imageViewHeight.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
}
}
public func scrollviewDidScroll(scrollView: UIScrollView) is the function that you need to call in your main ViewController's scrollViewDidScroll method.
Register above class to your collectioView:
collectionView.register(StretchyCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier)
Add this to your section header while creating layout for collectionView:
func generateFirstSectionLayout(isWide: Bool) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
// Set header properties here
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(isWide ? 2/4 : 2/3))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: HomePageViewController.sectionHeaderElementKind,
alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
return section
}
Add data to your header:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if indexPath.section == 0 {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier,
for: indexPath) as? StretchyCollectionHeaderView else { fatalError("Cannot create header view") }
supplementaryView.imageView.image = UIImage(named: "HeaderHomePage")
return supplementaryView
}
else {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HomePageAutoxHeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
return supplementaryView
}
}
Add to first section and then finally call this in your ViewController:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let header = homePageCollectionView.supplementaryView(forElementKind: HomePageViewController.sectionHeaderElementKind, at: IndexPath(item: 0, section: 0)) as? StretchyCollectionHeaderView {
header.scrollviewDidScroll(scrollView: homePageCollectionView)
}
}
In case your header only stretches after you start scrolling, call above function in viewDidAppear() too.

Related

UICollectionView doesn't configure nor prefetch the second item until scrolling

I use UICollectionView to present cells. Each cell takes up the full screen size. The collection view is created with several items while only the first one is configured and displayed on the screen.
The problem is that the second item is not configured nor prefetched unless collectionView is scrolled down.
In my use case cell configuration fetches data from remote server, which I prefer doing the sooner the better. When I scroll down but the second cell isn't configured there is nothing to present yet.
I suspected that the layout has something to do with it, so I tried to use UICollectionViewFlowLayout as well as UICollectionViewCompositionalLayout and in both cases the problem occurs.
Is it possible to force the collection view to call the configure method of the second cell earlier?
I created a demo swift project with collectionView presenting screen sized colored rectangles. Cell configuration and prefetch is logged to the console with their indexPath.
import Foundation
import UIKit
enum Section {
case main
}
typealias DataSource = UICollectionViewDiffableDataSource<Section, UIColor>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, UIColor>
class CollectionViewController: UIViewController, UICollectionViewDelegateFlowLayout,
UICollectionViewDataSourcePrefetching {
private lazy var dataSource: DataSource = makeDataSource()
private let collectionView: UICollectionView
private static let cellIdentifier = "CollectionViewCell"
init() {
// let layout = Self.makeFlowLayout()
let layout = Self.makeCompositionLayout()
print("Log: collectionview layout - \(layout.description)")
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(
UICollectionViewCell.self,
forCellWithReuseIdentifier: Self.cellIdentifier
)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
applySnapshot(animatingDifferences: false)
}
private static func makeFlowLayout() -> UICollectionViewFlowLayout {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
flowLayout.minimumLineSpacing = 0
return flowLayout
}
private static func makeCompositionLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 0
let fullSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: fullSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: fullSize, subitems: [item])
let layout = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: layout, configuration: config)
}
private func setupCollectionView() {
collectionView.backgroundColor = .white
collectionView.delegate = self
collectionView.prefetchDataSource = self
collectionView.isPrefetchingEnabled = true
collectionView.contentInsetAdjustmentBehavior = .never
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
let contraints = [
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor)
]
NSLayoutConstraint.activate(contraints)
}
private func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, itemIdentifier) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: Self.cellIdentifier,
for: indexPath
)
print("Log: configure cell in indexPath - \(indexPath)")
cell.backgroundColor = itemIdentifier
return cell
})
return dataSource
}
private func applySnapshot(animatingDifferences: Bool = true) {
let items = (0...200).map { _ in
UIColor(
red: CGFloat.random(in: 0...1),
green: CGFloat.random(in: 0...1),
blue: CGFloat.random(in: 0...1),
alpha: 1
)
}
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height)
}
// MARK: - UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
print("Log: prefetch cells in indexPaths - \(indexPaths)")
}
}

UICollectionViewCell is not being correctly called from the UIViewController

I am trying to create a custom overlay for a UICollectionViewCell that when a user selects an image it puts a gray overlay with a number (ie. order) that the user selected the image in. When I run my code I do not get any errors but it also appears to do nothing. I added some print statements to help debug and when I run the code I get "Count :0" printed 15 times. That is the number of images I have in the library. When I select the first image in the first row I still get "Count: 0" as I would expect, but when I select the next image I get the print out that you see below. It appears that the count is not working but I am not sure why. What am I doing wrong? I can't figure out why the count is wrong, but my primary issue/concern I want to resolve is why the overlay wont display properly?
Print Statement
Cell selected: [0, 0]
Count :0
Count :0
Count :0
Cell selected: [0, 4]
Count :0
View Controller
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.setupView()
print("Cell selected: \(indexPath)")
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.backgroundColor = nil
cell.imageView.alpha = 1
}
}
Custom Overlay
lazy var circleView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = self.countSize.width / 2
view.alpha = 0.4
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var countLabel: UILabel = {
let label = UILabel()
let font = UIFont.preferredFont(forTextStyle: .headline)
label.font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.bold)
label.textAlignment = .center
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private func setup(){addSubview(circleView)
addSubview(circleView)
addSubview(countLabel)
NSLayoutConstraint.activate([
circleView.leadingAnchor.constraint(equalTo: leadingAnchor),
circleView.trailingAnchor.constraint(equalTo: trailingAnchor),
circleView.topAnchor.constraint(equalTo: topAnchor),
circleView.bottomAnchor.constraint(equalTo: bottomAnchor),
countLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
countLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
countLabel.topAnchor.constraint(equalTo: topAnchor),
countLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
TestCVCell: UICollectionViewCell
override var isSelected: Bool {
didSet { overlay.isHidden = !isSelected }
}
var imageView: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.backgroundColor = UIColor.gray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var count: Int = 0 {
didSet { overlay.countLabel.text = "\(count)" }
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.isHidden = true
return view
}()
func setupView() {
addSubview(imageView)
addSubview(overlay)
print("Count :\(count)")
NSLayoutConstraint.activate([
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leftAnchor.constraint(equalTo: imageView.leftAnchor),
overlay.rightAnchor.constraint(equalTo: imageView.rightAnchor),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = self.bounds
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
fatalError("init(coder:) has not been implemented")
}
Based on your other question, I'm guessing you are trying to do something like this...
Display images from device Photos, and allow multiple selections in order:
and, when you de-select a cell - for example, de-selecting my 2nd selection - you want to re-number the remaining selections:
To accomplish this, you need to keep track of the cell selections in an array - as they are made - so you can maintain the numbering.
Couple ways to approach this... here is one.
First, I'd suggest re-naming your count property to index, and, when setting the value, show or hide the overlay:
var index: Int = 0 {
didSet {
overlay.countLabel.text = "\(index)"
// hide if count is Zero, show if not
overlay.isHidden = index == 0
}
}
When you dequeue a cell from cellForItemAt, see if the indexPath is in our "tracking" array and set the cell's .index property appropriately (which will also show/hide the overlay).
Next, when you select a cell:
add the indexPath to our tracking array
we can set the .index property - with the count of our tracking array - directly to update the cell's appearance, because it won't affect any other cells
When you de-select a cell, we have to do additional work:
remove the indexPath from our tracking array
reload the cells so they are re-numbered
Here is a complete example - lots of comments in the code.
CircleView
class CircleView: UIView {
// simple view subclass that keeps itself "round"
// (assuming it has a 1:1 ratio)
override func layoutSubviews() {
layer.cornerRadius = bounds.width * 0.5
}
}
CustomAssetCellOverlay
class CustomAssetCellOverlay: UIView {
lazy var circleView: CircleView = {
let view = CircleView()
view.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var countLabel: UILabel = {
let label = UILabel()
let font = UIFont.preferredFont(forTextStyle: .headline)
label.font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.bold)
label.textAlignment = .center
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private func setup(){addSubview(circleView)
addSubview(circleView)
addSubview(countLabel)
NSLayoutConstraint.activate([
// circle view at top-left
circleView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
circleView.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
// circle view Width: 28 Height: 1:1 ratio
circleView.widthAnchor.constraint(equalToConstant: 28.0),
circleView.heightAnchor.constraint(equalTo: circleView.widthAnchor),
// count label constrained ot circle view
countLabel.leadingAnchor.constraint(equalTo: circleView.leadingAnchor),
countLabel.trailingAnchor.constraint(equalTo: circleView.trailingAnchor),
countLabel.topAnchor.constraint(equalTo: circleView.topAnchor),
countLabel.bottomAnchor.constraint(equalTo: circleView.bottomAnchor),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
TestCVCell
class TestCVCell: UICollectionViewCell {
var imageView = UIImageView()
var index: Int = 0 {
didSet {
overlay.countLabel.text = "\(index)"
// hide if count is Zero, show if not
overlay.isHidden = index == 0
}
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
view.isHidden = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
contentView.addSubview(overlay)
imageView.translatesAutoresizingMaskIntoConstraints = false
overlay.translatesAutoresizingMaskIntoConstraints = false
// constrain both image view and overlay to full contentView
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
overlay.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
TrackSelectionsViewController
class TrackSelectionsViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
var myCollectionView: UICollectionView!
// array to track selected cells in the order they are selected
var selectedCells: [IndexPath] = []
// to load assests when needed
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
// will be used to get photos data
var fetchResult: PHFetchResult<PHAsset>!
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// request Options to be used in cellForItemAt
requestOptions.isSynchronous = false
requestOptions.deliveryMode = .opportunistic
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 0
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor, constant:0.0),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// create a label
let label = UILabel()
// add the label to the main stack view
mainStack.addArrangedSubview(label)
// label properties
label.textColor = .white
label.textAlignment = .center
label.text = "Select Photos"
label.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
// setup the collection view
setupCollection()
// add it to the main stack view
mainStack.addArrangedSubview(myCollectionView)
// start the async call to get the assets
grabPhotos()
}
func setupCollection() {
let layout = UICollectionViewFlowLayout()
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.backgroundColor = UIColor.white
myCollectionView.allowsMultipleSelection = true
myCollectionView.register(TestCVCell.self, forCellWithReuseIdentifier: "cvCell")
}
//MARK: CollectionView
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
// add newly selected cell (index path) to our tracking array
selectedCells.append(indexPath)
// when selecting a cell,
// we can update the appearance of the newly selected cell
// directly, because it won't affect any other cells
cell.index = selectedCells.count
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
// when de-selecting a cell,
// we can't update the appearance of the cell directly
// because if it's not the last cell selected, the other
// selected cells need to be re-numbered
// get the index of the deselected cell from our tracking array
guard let idx = selectedCells.firstIndex(of: indexPath) else { return }
// remove from our tracking array
selectedCells.remove(at: idx)
// reloadData() clears the collection view's selected cells, so
// get a copy of currently selected cells
let curSelected: [IndexPath] = collectionView.indexPathsForSelectedItems ?? []
// reload collection view
// we do this to update all cells' appearance,
// including re-numbering the currently selected cells
collectionView.reloadData()
// save current Y scroll offset
let saveY = collectionView.contentOffset.y
collectionView.performBatchUpdates({
// re-select previously selected cells
curSelected.forEach { pth in
collectionView.selectItem(at: pth, animated: false, scrollPosition: .centeredVertically)
}
}, completion: { _ in
// reset Y offset
collectionView.contentOffset.y = saveY
})
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard fetchResult != nil else { return 0 }
return fetchResult.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! TestCVCell
imgManager.requestImage(for: fetchResult.object(at: indexPath.item) as PHAsset, targetSize: CGSize(width:120, height: 120),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
cell.imageView.image = image
})
// get the index of this indexPath from our tracking array
// if it's not there (nil), set it to -1
let idx = selectedCells.firstIndex(of: indexPath) ?? -1
// set .count property to index + 1 (arrays are zero-based)
cell.index = idx + 1
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
return CGSize(width: width/4 - 1, height: width/4 - 1)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
//MARK: grab photos
func grabPhotos(){
DispatchQueue.global(qos: .background).async {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
if self.fetchResult.count == 0 {
print("No photos found.")
}
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
}
}
Note: This is example code only!!! It should not be considered "production ready."
Shouldn't your var count: Int = 0 be set at your CollectionView delegate?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.setupView()
cell.count = indexPath.item
print("Cell selected: \(indexPath)")
}
}

iOS UICollectionView Horizontal Scrolling Rectangular Layout with different size of items?

iOS UICollectionView how to Create Horizontal Scrolling rectangular layout with different size of items inside.
I want to create a Rectangular layout using UICollectionView like below. how can i achieve?
When i scroll horizontally using CollectionView 1,2,3,4,5,6 grid will scroll together to bring 7.
The Below are the dimensions of 320*480 iPhone Resolution. Updated Screen below.
First 6 items have below dimensions for iPhone 5s.
Item 1 Size is - (213*148)
Item 2 Size is - (106*75)
Item 3 Size is - (106*74)
Item 4 Size is - (106*88)
Item 5 Size is - (106*88)
Item 6 Size is - (106*88)
After item6 have same dimensions as collection View width and height like below.
Item 7 Size is - (320*237)
Item 8 Size is - (320*237)
Item 9 Size is - (320*237)
How to create a simple custom Layout Using collection view, that has horizontal scrolling?
Must appreciate for a quick solution. Thanks in advance.
I would suggest using a StackView inside CollectionViewCell(of fixed dimension) to create a grid layout as shown in your post.
Below GridStackView creates a dynamic grid layout based on the number of views added using method addCell(view: UIView).
Add this GridStackView as the only subview of your CollectionViewCell pinning all the edges to the sides so that it fills the CollectionViewCell completely.
while preparing your CollectionViewCell, add tile views to it using the method addCell(view: UIView).
If only one view added, then it will show a single view occupying whole GridStackView and so as whole CollectionViewCell.
If there is more than one view added, it will automatically layout them in the inside the CollectionViewCell.
You can tweak the code below to get the desired layout calculating the row and column. Current implementation needed rowSize to be supplied while initializing which I used for one of my project, you need to modify it a bit to get your desired layout.
class GridStackView: UIStackView {
private var cells: [UIView] = []
private var currentRow: UIStackView?
var rowSize: Int = 3
var defaultSpacing: CGFloat = 5
init(rowSize: Int) {
self.rowSize = rowSize
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
axis = .vertical
spacing = defaultSpacing
distribution = .fillEqually
}
required init(coder: NSCoder) {
super.init(coder: coder)
translatesAutoresizingMaskIntoConstraints = false
axis = .vertical
spacing = defaultSpacing
distribution = .fillEqually
}
private func preapreRow() -> UIStackView {
let row = UIStackView(arrangedSubviews: [])
row.spacing = defaultSpacing
row.translatesAutoresizingMaskIntoConstraints = false
row.axis = .horizontal
row.distribution = .fillEqually
return row
}
func removeAllCell() {
for item in arrangedSubviews {
item.removeFromSuperview()
}
cells.removeAll()
currentRow = nil
}
func addCell(view: UIView) {
let firstCellInRow = cells.count % rowSize == 0
if currentRow == nil || firstCellInRow {
currentRow = preapreRow()
addArrangedSubview(currentRow!)
}
view.translatesAutoresizingMaskIntoConstraints = false
cells.append(view)
currentRow?.addArrangedSubview(view)
setNeedsLayout()
}
}
Create a new cell that contains two views. Views have equal width.
Contstruct your data accordingly
Data
struct ItemData {
var color: [UIColor]
}
// NOTICE: 2nd item contains two colors and the rest one.
let data = [ItemData(color: [.red]), ItemData(color: [.blue, .purple]), ItemData(color: [.orange]),
ItemData(color: [.cyan]), ItemData(color: [.green]), ItemData(color: [.magenta]),
ItemData(color: [.systemPink]), ItemData(color: [.link]), ItemData(color: [.purple])]
Cell
class CollectionViewCellOne: UICollectionViewCell {
static let identifier = "CollectionViewCellOne"
var item: ItemData? {
didSet {
if let item = item {
self.leadingLabel.backgroundColor = item.color.first!
self.trailingLabel.backgroundColor = item.color.last!
}
}
}
let leadingLabel = UILabel()
let trailingLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.addSubview(leadingLabel)
self.contentView.addSubview(trailingLabel)
let width = self.frame.width / 2
leadingLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
leadingLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
leadingLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
leadingLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
leadingLabel.translatesAutoresizingMaskIntoConstraints = false
trailingLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
trailingLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
trailingLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
trailingLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
trailingLabel.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
fatalError()
}
}
dequeueReusableCell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCellOne.identifier, for: indexPath) as! CollectionViewCellOne
cell.item = data[indexPath.row]
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as! CollectionViewCell
if let color = data[indexPath.row].color.first {
cell.backgroundColor = color
}
return cell
}
}
I have tried with Mahan's Answer and i am getting the partially Correct output. But the issue is, index1 having full width of two items.
How to split index 1 into index1 and Index2?
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setUpCollectionView()
// Do any additional setup after loading the view.
}
func setUpCollectionView() {
self.view.backgroundColor = .white
let layout = UICollectionViewFlowLayout()
// layout.minimumInteritemSpacing = 1
// layout.minimumLineSpacing = 1
layout.scrollDirection = .horizontal
let collectionView = CollectionView(frame: .zero, collectionViewLayout: layout)
view.addSubview(collectionView)
collectionView.bounces = false
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: 240).isActive = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
}
}
class CollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.identifier)
self.dataSource = self
self.delegate = self
self.isPagingEnabled = true
}
required init?(coder: NSCoder) {
fatalError()
}
}
extension CollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as! CollectionViewCell
cell.backgroundColor = .blue
cell.label.text = "\(indexPath.row)"
let row = indexPath.row
switch row {
case 0:
cell.backgroundColor = .red
case 1:
cell.backgroundColor = .blue
case 2:
cell.backgroundColor = .purple
case 3:
cell.backgroundColor = .orange
case 4:
cell.backgroundColor = .cyan
case 5:
cell.backgroundColor = .green
case 6:
cell.backgroundColor = .magenta
case 7:
cell.backgroundColor = .white
case 8:
cell.backgroundColor = .blue
case 9:
cell.backgroundColor = .green
default:
cell.backgroundColor = .systemPink
}
return cell
}
}
extension CollectionView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let row = indexPath.row
let width = collectionView.frame.width
let other = width / 3
let height = collectionView.frame.height
let o_height = height / 3
switch row {
case 0:
return CGSize(width: other * 2, height: o_height * 2)
case 1:
return CGSize(width: other * 2, height: o_height)
case 2:
return CGSize(width: other, height: o_height)
case 3:
return CGSize(width: other, height: o_height)
case 4:
return CGSize(width: other, height: o_height)
case 5, 6, 7:
return CGSize(width: other, height: o_height)
default:
return CGSize(width: width, height: height)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return .leastNormalMagnitude
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return .leastNormalMagnitude
}
}
class CollectionViewCell: UICollectionViewCell {
static let identifier = "CollectionViewCell"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.addSubview(label)
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
fatalError()
}
}
How to devide index 1 into index1 and Index2 like below?
Thanks in advance!

iOS - Sticky headers with content inset - Header view is not scrolling like the cell

We tried to roll the UICollectionView like a list over the map. This is not to be confused with bottom sheet which snaps to a point (like low, mid and high). The collectionview's flowlayout property has sectionHeadersPinToVisibleBounds enabled. I have attached the sample project for your reference. Is there any way the header view can move to the top of the collectionview as the user scrolls?
Here is the sample project
Essential changes need for me to enter that state:
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: layout)
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.contentInset = UIEdgeInsets(top: drawerHeight, left: 0, bottom: 0, right: 0)
}
Here is a screenshot of what you would see:
Red colored view is the header view which is pinned. Do I need a custom layout to update its position as the user scrolls?
I wrote a custom StickyHeaderLayout inspired from this post. Here is the fix to my bug:
class StickyHeaderLayout: UICollectionViewFlowLayout {
override init() {
super.init()
configureLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureLayout()
}
private func configureLayout() {
self.sectionFootersPinToVisibleBounds = true
self.sectionHeadersPinToVisibleBounds = true
minimumLineSpacing = 0
minimumInteritemSpacing = 0
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
for attribute in attributes {
adjustAttributesIfNeeded(attribute)
}
return attributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) else { return nil }
adjustAttributesIfNeeded(attributes)
return attributes
}
func adjustAttributesIfNeeded(_ attributes: UICollectionViewLayoutAttributes) {
switch attributes.representedElementKind {
case UICollectionView.elementKindSectionHeader?:
adjustHeaderAttributesIfNeeded(attributes)
default:
break
}
}
private func adjustHeaderAttributesIfNeeded(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
guard attributes.indexPath.section == 0 else { return }
if collectionView.contentOffset.y <= 0 {
attributes.frame.origin.y = 0
} else {
attributes.frame.origin.y = collectionView.contentOffset.y
}
}
}
one section is work, but if you have great than one section, it will cause some prolems

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