I've been breaking my head over this for the past few weeks and I'm out of ideas..
I built my own UICollectionView
framework with a custom UICollectionViewFlowLayout
for vertical card paging and creating a card stack effect.
Here's a gif of what it looks like:
I'm trying to implement a feature that allows the user to scroll to a specific card at a specific index (using the scrollToItem
function for example).
Now the problem is that for some reason, when trying to scroll back to the previous card it doesn't work correctly.
Below is a recording of what happens, I've attached a tapGestureRecognizer to it, when I tap the focussed (currently centered) card, it should scroll to that index - 1 (so the previous card).
For some reason it only does this once the frame.origin.y
of the bottom card is above the frame.origin.maxY
of the focussed (currently centered card).
Notice that I'm tapping twice on the card to make it work, because the frame.origin.y
value of the bottom card needs to be lower than the frame.origin.maxY
of the top card for some reason for it to work.
collectionView.scrollToItem(at: convertIndexToIndexPath(for: index), at: .top, animated: animated)
and
if let frame = collectionView.layoutAttributesForItem(at: convertIndexToIndexPath(for: index))?.frame {
collectionView.scrollRectToVisible(frame, animated: true)
}
I've already figured out which line is causing the issue, inside the subclass of UICollectionViewFlowLayout
(called VerticalCardSwiperFlowLayout
), there is a function called updateCellAttributes
, which modifies some properties of the frames to be able to achieve this effect. The issue occurs on the following line:
let finalY = max(cvMinY, cardMinY)
Here's the function in question including a very extended explanation of how it works at the top in code comments:
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
Below we'll briefly explain how the effect of scrolling a card to the background instead of the top is achieved.
Keep in mind that (x,y) coords in views start from the top left (x: 0,y: 0) and increase as you go down/to the right,
so as you go down, the y-value increases, and as you go right, the x value increases.
The two most important variables we use to achieve this effect are cvMinY and cardMinY.
* cvMinY (A): The top position of the collectionView + inset. On the drawings below it's marked as "A".
This position never changes (the value of the variable does, but the position is always at the top where "A" is marked).
* cardMinY (B): The top position of each card. On the drawings below it's marked as "B". As the user scrolls a card,
this position changes with the card position (as it's the top of the card).
When the card is moving down, this will go up, when the card is moving up, this will go down.
We then take the max(cvMinY, cardMinY) to get the highest value of those two and set that as the origin.y of the card.
By doing this, we ensure that the origin.y of a card never goes below cvMinY, thus preventing cards from scrolling upwards.
```
+---------+ +---------+
| | | |
| +-A=B-+ | | +-A-+ | ---> The top line here is the previous card
| | | | | +--B--+ | that's visible when the user starts scrolling.
| | | | | | | |
| | | | | | | | | As the card moves down,
| | | | | | | | v cardMinY ("B") goes up.
| +-----+ | | | | |
| | | +-----+ |
| +--B--+ | | +--B--+ |
| | | | | | | |
+-+-----+-+ +-+-----+-+
```
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
var cvMinY = collectionView.bounds.minY + collectionView.contentInset.top
let cardMinY = attributes.frame.minY
var origin = attributes.frame.origin
let cardHeight = attributes.frame.height
if cvMinY > cardMinY + cardHeight + minimumLineSpacing + collectionView.contentInset.top {
cvMinY = 0
}
let finalY = max(cvMinY, cardMinY)
let deltaY = (finalY - cardMinY) / cardHeight
transformAttributes(attributes: attributes, deltaY: deltaY)
// Set the attributes frame position to the values we calculated
origin.x = collectionView.frame.width/2 - attributes.frame.width/2 - collectionView.contentInset.left
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
// Creates and applies a CGAffineTransform to the attributes to recreate the effect of the card going to the background.
fileprivate func transformAttributes(attributes: UICollectionViewLayoutAttributes, deltaY: CGFloat) {
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
if isPreviousCardVisible {
t = t.translatedBy(x: 0, y: (deltaY * translationScale))
}
attributes.transform = t
}
}
Here's the function that calls that specific code:
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
for object in items {
if let attributes = object as? UICollectionViewLayoutAttributes {
self.updateCellAttributes(attributes)
}
}
return items as? [UICollectionViewLayoutAttributes]
}
If you want to have a look for yourself, you can download the full project here: https://github.com/JoniVR/VerticalCardSwiper/archive/master.zip
(if you do checkout through github, make sure to download the development branch)
The specific Github issue with some discussion and possibly extra information can be found here.
Thank you for your time, any help or information with this problem would be deeply appreciated! Also, please let me know if you have any further question or if any crucial information is missing
edit 1:
Note: The weird effect only happens when scrolling to currentIndex - 1
, currentIndex + 1
doesn't give any issues, which I think somewhat explains why it's happening on let finalY = max(cvMinY, cardMinY)
, but I haven't figured out a proper solution for it yet.
I've managed to get it to work another way, by using setContentOffset
and calculating the offset manually.
guard index >= 0 && index < verticalCardSwiperView.numberOfItems(inSection: 0) else { return }
let y = CGFloat(index) * (flowLayout.cellHeight + flowLayout.minimumLineSpacing) - topInset
let point = CGPoint(x: verticalCardSwiperView.contentOffset.x, y: y)
verticalCardSwiperView.setContentOffset(point, animated: animated)
verticalCardSwiperView is basically just a subclass of UICollectionView
, I'm doing this because I want to narrow down what a user can access since this is a specific library and allowing full collectionView access would make things confusing.
If you do happen to know what exactly is causing this issue or how I can fix it better, I'd still be happy to find out :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With