Skip to content

Instantly share code, notes, and snippets.

@trilliwon
Created May 5, 2021 10:05
Show Gist options
  • Save trilliwon/9c13d55b348961fb0f5f4b556f080c8a to your computer and use it in GitHub Desktop.
Save trilliwon/9c13d55b348961fb0f5f4b556f080c8a to your computer and use it in GitHub Desktop.
class SpringEffectFlowLayout: UICollectionViewFlowLayout {
var dynamicAnimator: UIDynamicAnimator!
var visibleIndexPathsSet = Set<IndexPath>()
var visibleHeaderAndFooterSet = Set<IndexPath>()
private var latestDelta: CGFloat = 0
var scrollResistanceFactor: CGFloat?
override init() {
super.init()
dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let visibleRect = CGRect(origin: collectionView.bounds.origin, size: collectionView.frame.size).insetBy(dx: -100, dy: -100)
guard let itemsInVisibleRectArray = super.layoutAttributesForElements(in: visibleRect) else { return }
let itemsIndexPathsInVisibleRectSet = Set<IndexPath>(itemsInVisibleRectArray.map(\.indexPath))
/// Step 1: Remove any behaviours that are no longer visible.
let noLongerVisibleBehaviours = self.dynamicAnimator.behaviors.filter { behavior in
if let indexPath = ((behavior as? UIAttachmentBehavior)?.items.first as? UICollectionViewLayoutAttributes)?.indexPath {
return itemsIndexPathsInVisibleRectSet.contains(indexPath) == false
}
return false
}
noLongerVisibleBehaviours
.forEach { behavior in
dynamicAnimator.removeBehavior(behavior)
if let indexPath = ((behavior as? UIAttachmentBehavior)?.items.first as? UICollectionViewLayoutAttributes)?.indexPath {
visibleIndexPathsSet.remove(indexPath)
visibleHeaderAndFooterSet.remove(indexPath)
}
}
/// Step 2: Add any newly visible behaviours.
/// A "newly visible" item is one that is in the itemsInVisibleRect(Set|Array) but not in the visibleIndexPathsSet
let newlyVisibleItems = itemsInVisibleRectArray.filter { item in
if item.representedElementCategory == .cell {
return self.visibleIndexPathsSet.contains(item.indexPath) == false
} else {
return self.visibleHeaderAndFooterSet.contains(item.indexPath) == false
}
}
let touchLocation = collectionView.panGestureRecognizer.location(in: collectionView)
newlyVisibleItems.forEach { item in
var center = item.center
let springBehaviour = UIAttachmentBehavior(item: item, attachedToAnchor: center)
springBehaviour.length = 1.0
springBehaviour.damping = 0.8
springBehaviour.frequency = 1.0
/// If our touchLocation is not (0,0), we'll need to adjust our item's center "in flight"
if touchLocation != .zero {
if (self.scrollDirection == .vertical) {
let distanceFromTouch = abs(touchLocation.y - springBehaviour.anchorPoint.y)
var scrollResistance: CGFloat
if let scrollResistanceFactor = self.scrollResistanceFactor {
scrollResistance = distanceFromTouch / scrollResistanceFactor
} else {
scrollResistance = distanceFromTouch / 900.0
}
if latestDelta < 0 {
center.y += max(latestDelta, latestDelta * scrollResistance)
} else {
center.y += min(latestDelta, latestDelta * scrollResistance)
}
item.center = center
}
}
self.dynamicAnimator.addBehavior(springBehaviour)
if item.representedElementCategory == .cell {
visibleIndexPathsSet.insert(item.indexPath)
} else {
visibleHeaderAndFooterSet.insert(item.indexPath)
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let dynamicLayoutAttributes = dynamicAnimator.layoutAttributesForCell(at: indexPath)
/// Check if dynamic animator has layout attributes for a layout, otherwise use the flow layouts properties.
/// This will prevent crashing when you add items later in a performBatchUpdates block (e.g. triggered by NSFetchedResultsController update)
return dynamicLayoutAttributes ?? super.layoutAttributesForItem(at: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let scrollView = self.collectionView else { return false }
var delta: CGFloat
if scrollDirection == .vertical {
delta = newBounds.origin.y - scrollView.bounds.origin.y
} else {
delta = newBounds.origin.x - scrollView.bounds.origin.x
}
latestDelta = delta
let touchLocation = scrollView.panGestureRecognizer.location(in: collectionView)
dynamicAnimator.behaviors.forEach { behaviour in
guard let springBehaviour = behaviour as? UIAttachmentBehavior else { return }
if scrollDirection == .vertical {
let distanceFromTouch = abs(touchLocation.y - springBehaviour.anchorPoint.y)
var scrollResistance: CGFloat
if let scrollResistanceFactor = scrollResistanceFactor {
scrollResistance = distanceFromTouch / scrollResistanceFactor
} else {
scrollResistance = distanceFromTouch / 900.0
}
guard let item = springBehaviour.items.first as? UICollectionViewLayoutAttributes else { return }
var center = item.center
if delta < 0 {
center.y += max(delta, delta * scrollResistance)
} else {
center.y += min(delta, delta * scrollResistance)
}
item.center = center
dynamicAnimator.updateItem(usingCurrentState: item)
}
}
return false
}
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
updateItems.enumerated().forEach { index, item in
if item.updateAction == .insert {
guard let indexPath = item.indexPathAfterUpdate else { return }
if self.dynamicAnimator.layoutAttributesForCell(at: indexPath) != nil {
return
}
if let indexPathAfterUpdate = item.indexPathAfterUpdate {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPathAfterUpdate)
let springBehaviour = UIAttachmentBehavior(item: attributes, attachedToAnchor: attributes.center)
springBehaviour.length = 1.0
springBehaviour.damping = 0.8
springBehaviour.frequency = 1.0
dynamicAnimator.addBehavior(springBehaviour)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment