When a cell is fully visible at the center of the collection view, I want it to appear larger than the cells in the sides.
Currently, I'm utilizing visibleItemsInvalidationHandler
closure to achieve that effect:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let ratio = (inter.width * inter.height) / (frame.width * frame.height)
let scale = ratio > 0.8 ? ratio : 0.8
UIView.animate(withDuration: 0.2) {
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
}
However, I still don't get the result I'm aiming for. This is what I get:
The cells start to flicker when swiped, and when I try to get a cell to the center, the result value from max(ratio, scale)
is less than what it was before. I want the value of max(ratio, scale)
to increase, the more the cell is visible on the screen.
Ideally, this is what I'm aiming for:
MRE: ViewController, enums and structs:
struct BannerEntity: Hashable {
private let id = UUID()
let bannerTitle: String
init(bannerTitle: String) {
self.bannerTitle = bannerTitle
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: BannerEntity,
rhs: BannerEntity) -> Bool {
return lhs.id == rhs.id
}
}
struct HomePresentationModel {
var section: HomeSection
var items: [HomeSectionItem]
}
enum HomeSectionItem: Hashable {
case banner(BannerEntity)
}
struct HomeSection: Hashable {
var header: HomeSectionHeader
let sectionType: HomeSectionType
}
enum HomeSectionType: Int, Hashable {
case banner
}
enum HomeSectionHeader: Int, Hashable {
case tappableHeader
case empty
}
class ViewController: UIViewController {
// MARK: Subviews
private var collectionView: UICollectionView!
// MARK: Properties
private lazy var dataSource = makeDataSource()
private var sections = [HomePresentationModel(section: .init(header: .empty, sectionType: .banner),
items: [.banner(.init(bannerTitle: "firstBanner")),
.banner(.init(bannerTitle: "secondBanner")),
.banner(.init(bannerTitle: "thirdBanner")),
.banner(.init(bannerTitle: "fourthBanner"))]
)]
// MARK: Value Type
typealias DataSource = UICollectionViewDiffableDataSource<
HomeSection,
HomeSectionItem
>
typealias Snapshot = NSDiffableDataSourceSnapshot<
HomeSection,
HomeSectionItem
>
// MARK: Viewcycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
configureCollectionView()
applySnapshot()
}
// MARK: Helpers
private func configureCollectionView(){
collectionView = UICollectionView(frame: view.frame,
collectionViewLayout: generateLayout())
view.addSubview(collectionView)
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(BannerCell.self,
forCellWithReuseIdentifier: "BannerCell")
}
private func generateLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
[self] (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment)
-> NSCollectionLayoutSection? in
let sectionType = HomeSectionType(rawValue: sectionIndex)
guard sectionType == .banner else {return nil}
return self.generateBannersLayout()
}
return layout
}
func generateBannersLayout() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9),
heightDimension: .fractionalHeight(0.28))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
group.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 0,
bottom: 0,
trailing: 0)
group.interItemSpacing = NSCollectionLayoutSpacing.fixed(0)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 10,
leading: 0,
bottom: 10,
trailing: 0)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let ratio = (inter.width * inter.height) / (frame.width * frame.height)
let scale = ratio > 0.8 ? ratio : 0.8
UIView.animate(withDuration: 0.2) {
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}
}
return section
}
private func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, item) ->
UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "BannerCell",
for: indexPath) as? BannerCell
return cell
})
return dataSource
}
private func applySnapshot() {
var snapshot = Snapshot()
snapshot.appendSections(sections.map({$0.section}))
sections.forEach { section in
snapshot.appendItems(section.items, toSection: section.section)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}
BannerCell:
import UIKit
class BannerCell: UICollectionViewCell {
// MARK: Init
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Helpers
private func setupSubviews(){
backgroundColor = .purple
layer.cornerRadius = 10
}
}
You're close...
First, no need for the UIView.animate
block. .visibleItemsInvalidationHandler
is called for each layout cycle, so it's triggered (effectively) continuously as we scroll the section.
Second, your height scale calculation is not-quite-right...
You're getting the intersection (CGRect
) of the cell frame with the view frame, so we want the height scale to match the percentage of the width of the intersecting rect. Well, in this case, the percentage of the range between 0.8
and 1.0
.
If you change your .visibleItemsInvalidationHandler
to this, it should be what you're going for -- or at least close enough that you can tweak it to your satisfaction:
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let frame = item.frame
let rect = CGRect(x: offset.x, y: offset.y, width: environment.container.contentSize.width, height: frame.height)
let inter = rect.intersection(frame)
let percent: CGFloat = inter.width / frame.width
let scale = 0.8 + (0.2 * percent)
item.transform = CGAffineTransform(scaleX: 0.98, y: scale)
}
}