Skip to content

Instantly share code, notes, and snippets.

@strzempa
Created February 5, 2021 18:05
Show Gist options
  • Save strzempa/83680843954ebda79b6cf5808e5d6ddb to your computer and use it in GitHub Desktop.
Save strzempa/83680843954ebda79b6cf5808e5d6ddb to your computer and use it in GitHub Desktop.
UICollectionView performs batch update with custom floating animation of the new cell
import UIKit
private func makeRandomData() -> [MyModel] { [ MyModel(color: .random) ] }
private final class MyUICollectionViewFlowLayout: UICollectionViewFlowLayout {
var insertingIndexPaths = [IndexPath]()
override func prepare(
forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]
) {
super.prepare(forCollectionViewUpdates: updateItems)
insertingIndexPaths.removeAll()
updateItems.forEach { update in
guard let indexPath = update.indexPathAfterUpdate,
update.updateAction == .insert else {
return
}
insertingIndexPaths.append(indexPath)
}
}
override func finalizeCollectionViewUpdates() {
super.finalizeCollectionViewUpdates()
insertingIndexPaths.removeAll()
}
override func initialLayoutAttributesForAppearingItem(
at itemIndexPath: IndexPath
) -> UICollectionViewLayoutAttributes? {
let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
if insertingIndexPaths.contains(itemIndexPath) {
attributes?.alpha = 0.0
attributes?.transform
= CGAffineTransform(
translationX: 200,
y: 0
)
}
return attributes
}
}
final class ViewController: UIViewController {
private let collectionView: MyCollectionViewController = {
let layout = MyUICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
return MyCollectionViewController(collectionViewLayout: layout)
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
}
private extension ViewController {
func setupUI() {
view.addSubview(collectionView.view)
collectionView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.view.heightAnchor.constraint(equalToConstant: 300),
collectionView.view.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.view.rightAnchor.constraint(equalTo: view.rightAnchor)
])
collectionView.data = makeRandomData()
collectionView.collectionView.reloadData()
}
}
private final class MyCollectionViewController: UICollectionViewController {
var data: [MyModel] = []
override init(collectionViewLayout layout: UICollectionViewLayout) {
super.init(collectionViewLayout: layout)
collectionView.contentInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 180)
collectionView.register(MyCell.self, forCellWithReuseIdentifier: MyCell.identifier)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
data.count
}
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
guard let cell
= collectionView.dequeueReusableCell(
withReuseIdentifier: MyCell.identifier,
for: indexPath
) as? MyCell else {
return UICollectionViewCell()
}
cell.configure(with: data[indexPath.row])
return cell
}
override func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
guard indexPath.row == data.count - 1 else {
return
}
let oldData = data
let newData = data + makeRandomData()
data = newData
UIView.animate(
withDuration: 1.1,
delay: 0,
usingSpringWithDamping: 0.71,
initialSpringVelocity: 0.3,
options: [.allowUserInteraction, .layoutSubviews, .curveEaseInOut]
) {
collectionView.performBatchUpdates { [weak self] in
let diff = newData.difference(from: oldData)
diff.forEach { change in
switch change {
case let .remove(offset, _, _):
let indexes = [IndexPath(item: offset, section: 0)]
self?.collectionView.deleteItems(at: indexes)
case let .insert(offset, _, _):
let indexes = [IndexPath(item: offset, section: 0)]
self?.collectionView.insertItems(at: indexes)
}
}
} completion: { _ in }
} completion: { _ in }
}
}
extension MyCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
CGSize(width: 200, height: 200)
}
}
private struct MyModel: Hashable, Identifiable {
let id: UUID = UUID()
let color: UIColor
func hash(into hasher: inout Hasher) {
hasher.combine(color)
hasher.combine(id)
}
}
private final class MyCell: UICollectionViewCell {
func configure(with model: MyModel) {
contentView.backgroundColor = model.color
}
}
private extension UICollectionViewCell {
static var identifier: String {
String(describing: self)
}
}
private extension UIColor {
static var random: UIColor {
UIColor(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1.0)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment