Created
February 5, 2021 18:05
-
-
Save strzempa/83680843954ebda79b6cf5808e5d6ddb to your computer and use it in GitHub Desktop.
UICollectionView performs batch update with custom floating animation of the new cell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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