This one liner to add beautiful transition between UICollectionViews
UIKit comes with a bag full of techniques to build mind blowing animations and transitions but this one liner has to my favorite.
useLayoutToLayoutNavigationTransitions = true
So how do you make it work? Simple first you need to have 2 UICollectionViewController instances in a UINavigationController. Then just before you push the second screen set the useLayoutToLayoutNavigationTransitions = true. And done!
Did I say 2 UICollectionViewController? Sorry I mean one. The only thing that is changing is the collectionViewLayout that is powering the UICollectionView.
class TileLayout: UICollectionViewFlowLayout {
let scale: CGFloat
init(size: CGSize, scale: CGFloat) {
self.scale = scale
super.init()
let edge = (size.width * scale) - 12
itemSize = CGSize(width: edge, height: edge)
sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
minimumInteritemSpacing = 0
minimumLineSpacing = 8
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct DataSource {
let colors: [UIColor]
init(colors: [UIColor]) {
self.colors = colors
}
init() {
let count = 48
let colors = (0..<count)
.map { 1 - (CGFloat($0) / CGFloat(count)) }
.map { UIColor(hue: $0, saturation: 0.7, brightness: 0.8, alpha: 1) }
self.init(colors: colors)
}
}
class ViewController: UICollectionViewController {
static let cellId = "cell-id"
let dataSource: DataSource
private var selectedIndex: IndexPath?
private var scale: CGFloat {
(collectionViewLayout as? TileLayout)?.scale ?? 1
}
init(
dataSource: DataSource,
selectedIndex: IndexPath?,
collectionViewLayout: UICollectionViewLayout
) {
self.dataSource = dataSource
self.selectedIndex = selectedIndex
super.init(collectionViewLayout: collectionViewLayout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoom: \(scale)"
collectionView.showsVerticalScrollIndicator = false
collectionView.register(
UICollectionViewCell.self,
forCellWithReuseIdentifier: ViewController.cellId
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let selectedIndex {
collectionView.scrollToItem(
at: selectedIndex,
at: .centeredVertically,
animated: false
)
}
}
override func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
dataSource.colors.count
}
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: ViewController.cellId,
for: indexPath
)
cell.backgroundColor = dataSource.colors[indexPath.item]
return cell
}
override func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
selectedIndex = indexPath
let detailsVwCtrl = ViewController(
dataSource: dataSource,
selectedIndex: indexPath,
collectionViewLayout: TileLayout(
size: view.bounds.size,
scale: min(scale * 2, 1.0)
)
)
detailsVwCtrl.useLayoutToLayoutNavigationTransitions = true
navigationController?.pushViewController(
detailsVwCtrl,
animated: true
)
collectionView.deselectItem(at: indexPath, animated: true)
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = UINavigationController(
rootViewController: ViewController(
dataSource: DataSource(),
selectedIndex: nil,
collectionViewLayout: TileLayout(
size: windowScene.screen.bounds.size,
scale: 0.25
)
)
)
window?.makeKeyAndVisible()
}
// ...
}