I’ve a horizontally scrolling UICollectionView
in Swift.
Inside scrollViewDidScroll
, I apply a rotation and scale rework to seen cells to create a card-tilt impact.
The difficulty:
If I double-tap or use two fingers on the gathering view throughout scrolling, the gathering view turns into “frozen” — it stops responding to swipe gestures, and the centered cell stays caught in place till I reload knowledge.
It looks as if making use of a rework in scrollViewDidScroll
interferes with the gathering view’s contact dealing with and hit-testing, however I can’t work out easy methods to forestall it with out eradicating the rework impact.
How can I preserve the rework impact and stop the gathering view from freezing after multi-touch or double-tap?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let collectionView = scrollView as? UICollectionView else { return }
for cell in collectionView.visibleCells {
// 1. Calculate the cell's horizontal distance from the middle of the display
let centerX = view.bounds.width / 2
let cellCenter = collectionView.convert(cell.middle, to: view)
let distance = centerX - cellCenter.x
// 2. Calculate rotation and scale based mostly on this distance
// The farther from the middle, the extra it rotates and shrinks.
let maxDistance = collectionView.bounds.width / 2
let normalizedDistance = distance / maxDistance // Worth from -1 to 1
let maxAngle = CGFloat.pi / 30 // A refined angle (e.g., 6 levels)
let angle = maxAngle * normalizedDistance
let minScale: CGFloat = 0.9
let scale = 1.0 - (abs(normalizedDistance) * (1.0 - minScale))
// 3. Apply the rework
UIView.animate(withDuration: 0.3, delay: 0, choices: [.beginFromCurrentState, .allowUserInteraction], animations: {
cell.rework = CGAffineTransform(rotationAngle: angle).scaledBy(x: scale, y: scale)
}, completion: nil)
}
// ✅ Set preliminary centered index as soon as after format go
if !hasSetInitialCenteredIndex {
hasSetInitialCenteredIndex = true
DispatchQueue.predominant.asyncAfter(deadline: .now() + 0.05) {
self.snapToNearestCell()
self.applyTransformToVisibleCells()
}
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.cardsCollectionView.isUserInteractionEnabled = false
isScrolling = true
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
currentScrollDirection = velocity.x == 0 ? 0 : (velocity.x > 0 ? 1 : -1)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.cardsCollectionView.isUserInteractionEnabled = true
isScrolling = false
let pageWidth = scrollView.body.measurement.width
let currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
if currentPage == 0 {
let indexPath = IndexPath(merchandise: infinitePlaceholderArray.rely - 2, part: 0)
cardsCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
// Delay snapping till format is corrected
DispatchQueue.predominant.asyncAfter(deadline: .now() + 0.01) {
self.snapToNearestCell()
UIView.animate(withDuration: 0.3) {
self.applyTransformToVisibleCells()
}
}
return
} else if currentPage == infinitePlaceholderArray.rely - 1 {
let indexPath = IndexPath(merchandise: 1, part: 0)
cardsCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
// Delay snapping
DispatchQueue.predominant.asyncAfter(deadline: .now() + 0.01) {
self.snapToNearestCell()
UIView.animate(withDuration: 0.3) {
self.applyTransformToVisibleCells()
}
}
return
}
// No wrapping, snap usually
snapToNearestCell()
UIView.animate(withDuration: 0.3) {
self.applyTransformToVisibleCells()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.cardsCollectionView.isUserInteractionEnabled = true
isScrolling = false
snapToNearestCell()
UIView.animate(withDuration: 0.3) {
self.applyTransformToVisibleCells()
}
}
}
func setupInfiniteDataSource() {
// Ensure you have knowledge to work with
guard !placeholderArray.isEmpty else { return }
// [Last Item] + [All Original Items] + [First Item]
infinitePlaceholderArray.append(placeholderArray.final!)
infinitePlaceholderArray.append(contentsOf: placeholderArray)
infinitePlaceholderArray.append(placeholderArray.first!)
}
func applyTransformToVisibleCells() {
guard let collectionView = cardsCollectionView else { return }
for cell in collectionView.visibleCells {
let centerX = view.bounds.width / 2
let cellCenter = collectionView.convert(cell.middle, to: view)
let distance = centerX - cellCenter.x
let maxDistance = collectionView.bounds.width / 2
let normalizedDistance = distance / maxDistance
let maxAngle = CGFloat.pi / 30
let angle = maxAngle * normalizedDistance
let minScale: CGFloat = 0.9
let scale = 1.0 - (abs(normalizedDistance) * (1.0 - minScale))
cell.rework = CGAffineTransform(rotationAngle: angle).scaledBy(x: scale, y: scale)
}
}
non-public func snapToNearestCell() {
guard let collectionView = cardsCollectionView else { return }
let centerX = collectionView.bounds.measurement.width / 2 + collectionView.contentOffset.x
var closestIndexPath: IndexPath?
var closestDistance: CGFloat = .greatestFiniteMagnitude
for cell in collectionView.visibleCells {
let cellCenterX = cell.middle.x
let distance = abs(cellCenterX - centerX)
if distance < closestDistance {
closestDistance = distance
closestIndexPath = collectionView.indexPath(for: cell)
}
}
if let indexPath = closestIndexPath {
currentlyCenteredIndexPath = indexPath // Observe centered cell
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
non-public func handleInfiniteScrollWrapping(for scrollView: UIScrollView) {
let pageWidth = scrollView.body.measurement.width
let currentPage = Int(flooring((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
if currentPage == 0 {
let targetIndexPath = IndexPath(merchandise: infinitePlaceholderArray.rely - 2, part: 0)
cardsCollectionView.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false)
} else if currentPage == infinitePlaceholderArray.rely - 1 {
let targetIndexPath = IndexPath(merchandise: 1, part: 0)
cardsCollectionView.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false)
}
}
}
I attempted:
-
Detecting a number of touches in
touchesBegan
and calling mysnapToNearestCell()
technique to pressure snapping. -
Quickly disabling
isUserInteractionEnabled
on the gathering view throughout snap animations. -
Forcing
scrollViewWillEndDragging
andscrollViewDidEndDecelerating
logic to run manually after multi-touch.
Anticipated:
The gathering view ought to snap to the closest cell after a multi-touch occasion and stay scrollable as regular.
Precise end result:
After a double-tap or two-finger contact whereas scrolling, the gathering view turns into caught. Scrolling stops working fully, and I’ve to reload the gathering view to revive interplay.