UICollectionViewFlowLayout edge cells disappear when translation transform is applied - ios

I'm using a custom UICollectionViewFlowLayout to make cells zoom and fade as they reach the top. To do this, I'm applying an alpha and transform to the layout attributes. Here's my code (link to full demo repo):
class EdgeZoomLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let positionInFrameY = attributes.center.y - visibleRect.origin.y /// y origin of rectangle
let cutoff = CGFloat(30)
if positionInFrameY <= cutoff {
let translation = cutoff - positionInFrameY /// distance from the cutoff, 0 if exactly on cutoff
let alpha = 1 - (translation / 100)
let scale = 1 - (translation / 1000)
attributes.alpha = alpha
attributes.zIndex = Int(alpha >= 1 ? 1 : 0) /// if alpha is 1, keep on the top
attributes.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 0, y: translation)
} else {
attributes.zIndex = 1 /// keep on top if not getting zoomed
}
}
return rectAttributes
}
/// boilerplate code
override init() { super.init() }
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
This is almost perfect, but the topmost cell disappears suddenly when it gets close to the edge:
How can I make the cell zoom and fade without vanishing like that? I think it might be because the collection view recycled the cell... but then, how do I keep it?

Either you can remove this line
where attributes.frame.intersects(visibleRect)
or you can add some point for visibleRect by this
for attributes in rectAttributes where attributes.frame.intersects(visibleRect.insetBy(dx: 0, dy: -15)) {

Related

UICollectionView Cell Anchor Point not working

I am trying to achieve a collection view where the cells are aligned at the bottom with a paging effect where the "selected" cell is bigger than the rest. Like this:
As of now, I am able to get the effect to work but the cells are aligned in the middle instead of at the bottom:
I have tried setting the anchorPoint property of the cell to pin the cells at (0, 1) in apply(_ layoutAttributes: UICollectionViewLayoutAttributes), but this causes the cells to move and as as a result they appear cut off. This ends up looking like this:
How do I pin these collection view cells at the bottom left corner, also respecting the CGAffine scale effect that occurs during paging?
Here is my code:
Custom UICollectionViewFlowLayout:
import Foundation
import UIKit
/// The layout used in the cover flow.
class CoverFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 25
let zoomFactor: CGFloat = (CoverFlowCell.selectedSize / CoverFlowCell.unselectedSize) - 1
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
let itemSpace = itemSize.width + minimumInteritemSpacing
var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)
let vX = velocity.x
if vX > 0 {
currentItemIdx += 1
} else if vX < 0 {
currentItemIdx -= 1
}
let nearestPageOffset = currentItemIdx * itemSpace
return CGPoint(x: nearestPageOffset, y: 0)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = (visibleRect.minX + 20) - attributes.frame.minX
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform = CGAffineTransform(scaleX: zoom, y: zoom)
}
}
return rectAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
Custom UICollectionView Cell
import Foundation
import UIKit
class CoverFlowCell: UICollectionViewCell {
static let unselectedSize: CGFloat = 185; // The size of the cell when it is not selected in the carousel
static let selectedSize: CGFloat = 200;
private var albumArt: UIImageView = {
let art = UIImageView()
art.backgroundColor = UIColor(hexString: "#ECF0F1")
art.translatesAutoresizingMaskIntoConstraints = false
art.layer.cornerRadius = 2
return art
}()
/// Initializer
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupUIConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
}
private func setupUI() {
contentView.addSubview(albumArt)
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
//we must change the anchor point for propper cells positioning and scaling
self.layer.anchorPoint.x = 0
self.layer.anchorPoint.y = 1
}
private func setupUIConstraints() {
NSLayoutConstraint.activate([
self.albumArt.topAnchor.constraint(equalTo: topAnchor),
self.albumArt.bottomAnchor.constraint(equalTo: bottomAnchor),
self.albumArt.leftAnchor.constraint(equalTo: leftAnchor),
self.albumArt.rightAnchor.constraint(equalTo: rightAnchor)
])
}
}
I have tried referring to this thread:
Changing my CALayer's anchorPoint moves the view
But the solution provided did not help align the cells at the bottom.
Thanks
Add another transform to translate the y position, to slide it up after you scale it up:
let y = //set a negative number here, to slide up by that many points
transform = CGAffineTransform(translationX: 0, y: y)
I would also just apply the transform in the "did select item" method, rather than fuss with it in the layout attributes methods. Then when the cell is "deselected", you can just set the transform to .identity to reset it back to the normal layout.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//set transforms
}
func collectionView(_ UICollectionView, didDeselectItemAt: IndexPath) {
//set transform to .identity
}

How archive like this CollectionView stacked Cell for Overlapping Cell Like Wallet

I want exactly when tap particular cell it will show expand
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = -UIScreen.main.bounds.width/2.08
i want expand cell?
Friends any suggestion and needs helpful for us
Thanks
Usually we use UICollectionViewFlowLayout, once the item's size and spacing is given, the item's frame is settled.
To do
CollectionView stacked Cell for Overlapping Cell Like Wallet
You need to give the frame you want for every item in UICollectionView.
Finishing it by customizing UICollectionViewLayout, and use your own UICollectionViewLayout subclass.
The final effect is as the following image
The basic thing:
To do custom layout is easy:
override public func prepare() ,
to calculate the frames for every item,
And put the frame for item in its container , your custom UICollectionViewLayoutAttributes
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? ,
Retrieves layout information for an item at the specified index path with a corresponding cell.
to assign your prepared custom UICollectionViewLayoutAttributes for item layout
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? ,
Retrieves the layout attributes for all of the cells and views in the specified rectangle.
There is no supplementary views, and decoration views in this situation, just to handle items.
to assign your prepared custom UICollectionViewLayoutAttributes for UICollectionView layout
Key Point:
You have two kinds of layout state.
the initial state, and the unselected state is the same, when no card is selected.
The front card are partly hidden, and the last card is full shown.
The custom UICollectionViewLayoutAttributes, with isExpand to record if there is cell selected.
isExpand is used for an cell added UIPanGestureRecognizer
class CardLayoutAttributes: UICollectionViewLayoutAttributes {
var isExpand = false
override func copy(with zone: NSZone? = nil) -> Any {
let attribute = super.copy(with: zone) as! CardLayoutAttributes
attribute.isExpand = isExpand
return attribute
}
}
The frame calculation of state unselected is easy.
Set the frame of the first item,
then the second ...
y: titleHeight * CGFloat(index) , all OK
fileprivate func setNoSelect(attribute:CardLayoutAttributes) {
guard let collection = collectionView else {
return
}
let noneIdx = Int(collection.contentOffset.y/titleHeight)
if noneIdx < 0 {
return
}
attribute.isExpand = false
let index = attribute.zIndex
var currentFrame = CGRect(x: collection.frame.origin.x, y: titleHeight * CGFloat(index), width: cellSize.width, height: cellSize.height)
if index == noneIdx{
attribute.frame = CGRect(x: currentFrame.origin.x, y: collection.contentOffset.y, width: cellSize.width, height: cellSize.height)
}
else if index <= noneIdx, currentFrame.maxY > collection.contentOffset.y{
currentFrame.origin.y -= (currentFrame.maxY - collection.contentOffset.y )
attribute.frame = currentFrame
}
else {
attribute.frame = currentFrame
}
}
the selected state, to select an item, the item is expanded , others will make room for it.
The logic is to put the selected item in the middle, the y position matters, collection.contentOffset.y + offsetSelected,
The center item's frame is known , then calculate the two sides.
One side is items from (selectedIdx-1) to 0, calculate the frame of item.
The Other side is items from (selectedIdx+1) to the final index , also calculate item's frame.
fileprivate func calculate(for attributes: [CardLayoutAttributes], choose selectedIP: IndexPath) -> [CGRect]{
guard let collection = collectionView else {
return []
}
let noneIdx = Int(collection.contentOffset.y / titleHeight)
if noneIdx < 0 {
return []
}
let x = collection.frame.origin.x
var selectedIdx = 0
for attr in attributes{
if attr.indexPath == selectedIP{
break
}
selectedIdx += 1
}
var frames = [CGRect](repeating: .zero, count: attributes.count)
// Edit here
let offsetSelected: CGFloat = 100
let marginBottomSelected: CGFloat = 10
frames[selectedIdx] = CGRect(x: x, y: collection.contentOffset.y + offsetSelected, width: cellSize.width, height: cellSize.height)
if selectedIdx > 0{
for i in 0...(selectedIdx-1){
frames[selectedIdx - i - 1] = CGRect(x: x, y: frames[selectedIdx].origin.y - titleHeight * CGFloat(i + 1), width: cellSize.width, height: cellSize.height)
}
}
if selectedIdx < (attributes.count - 1){
for i in (selectedIdx + 1)...(attributes.count - 1){
frames[i] = CGRect(x: x, y: frames[selectedIdx].origin.y + marginBottomSelected + titleHeight * CGFloat(i - selectedIdx - 1) + cellSize.height, width: cellSize.width, height: cellSize.height)
}
}
return frames
}
When there is a item selected, you should refresh the custom layout.
To call invalidateLayout(),
Invalidates the current layout and triggers a layout update.
fileprivate var _selectPath: IndexPath? {
didSet {
self.collectionView!.isScrollEnabled = (_selectPath == nil)
}
}
public var selectPath: IndexPath? {
set {
_selectPath = (_selectPath == newValue) ? nil : newValue
self.collectionView?.performBatchUpdates({
self.invalidateLayout()
}, completion: nil)
} get {
return _selectPath
}
}
one more thing, the sample demo in github
Here is the overlapping the layout, minimumLineSpacing negative number
if click the card it should be expanded and scroll down and up card
should not disclose anymoew like wallet
class OverlappedCustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
// This allows us to make intersection and overlapping
// A negative number implies overlapping whereas positive implies space between the adjacent edges of two cells.
minimumLineSpacing = -100
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
for currentLayoutAttributes: UICollectionViewLayoutAttributes in layoutAttributes! {
// zIndex - Specifies the item’s position on the z-axis.
// Unlike a layer's zPosition, changing zIndex allows us to change not only layer position,
// but tapping/UI interaction logic too as it moves the whole item.
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row + 1
}
return layoutAttributes
}
}
you can try the following idea
The layout part:
var selectedIdx: Int?
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
var originY: CGFloat = 0
let defaultOffsetY: CGFloat = 80
if let idx = selectedIdx{
// expanded layout
for i in 0..<cnt{
// frame the attribute
if i == idx + 1{
// to expand is to make room for the rest items
originY += 400
}
else{
originY += defaultOffsetY
}
}
}
else{
// default layout
for i in 0..<cnt{
// frame the attribute
originY += defaultOffsetY
}
}
return layoutAttributes
}
trigger the expand action
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// edit the state
layout.selectedIdx = indexPath.item
// trigger refreshing the collectionView's layout
collectionView.reloadData()
}
I have tried it's working fine, thanks to everyone
class CardLayout: UICollectionViewLayout {
var contentHeight: CGFloat = 0.0
var cachedAttributes = [UICollectionViewLayoutAttributes]()
var nextIndexPath: Int?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
let collection = collectionView!
size.width = collection.bounds.size.width
if let _ = FlowLayoutAttributes.shared.cellIndex{
size.height = contentHeight+UIScreen.main.bounds.width/2+38
}else{
size.height = contentHeight
}
print("Contend",contentHeight)
return size
}
func reloadData(){
self.cachedAttributes = [UICollectionViewLayoutAttributes]()
}
override func prepare() {
cachedAttributes.removeAll()
guard let numberOfItems = collectionView?.numberOfItems(inSection: 0) else {
return
}
for index in 0..<numberOfItems {
let layout = UICollectionViewLayoutAttributes(forCellWith: IndexPath(row: index, section: 0))
layout.frame = frameFor(index: index)
if let indexExpand = FlowLayoutAttributes.shared.cellIndex, indexExpand == index {
self.nextIndexPath = index+1
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38*2
}else{
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38
}
layout.zIndex = index
layout.isHidden = false
cachedAttributes.append(layout)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cachedAttributes {
if attributes.frame.intersects(rect) {
layoutAttributes.append(cachedAttributes[attributes.indexPath.item])
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
func frameFor(index: Int) -> CGRect {
var frame = CGRect(origin: CGPoint(x: CGFloat(8), y:0), size: CGSize(width: UIScreen.main.bounds.width - CGFloat(8 + 8), height: CGFloat(UIScreen.main.bounds.width/2+38)))
var frameOrigin = frame.origin
if let indexExpand = FlowLayoutAttributes.shared.cellIndex{
if index > 0 {
if indexExpand < index {
let spacesHeight = CGFloat((getCardSize() * CGFloat(index)))+UIScreen.main.bounds.width/2+38-getCardSize()/2
frameOrigin.y = spacesHeight
}else{
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
}else{
if index > 0 {
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
frame.origin = frameOrigin
return frame
}
func getCardSize()-> CGFloat{
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 1136:
print("iPhone 5 or 5S or 5C")
return 45.25
case 1334:
print("iPhone 6/6S/7/8")
return 45.25
case 1920, 2208:
print("iPhone 6+/6S+/7+/8+")
return 46
case 2436:
print("iPhone X/XS/11 Pro")
return 45.25
case 2688:
print("iPhone XS Max/11 Pro Max")
return 46
case 1792:
print("iPhone XR/ 11 ")
return 46
case 2532:
print("iPhone 12/ iPhone 12 Pro")
return 45.50
case 2778:
print("iPhone 12 Pro Max")
return 46.2
default:
return 46.2
}
}else{
return CGFloat(46.2)
}
}
}

UIKit Dynamics in Collection View always ends in chaos

When scrolling with my "springy flow layout" (meant to replicate the scrolling effect in Messages), the initial scroll works well, but repeated scrolling ends with all of the cells constantly bouncing all over the screen, and off the vertical axis.
I don't understand why the cells are moving off the vertical axis, since there is no movement applied in the horizontal axis. I'm simply applying this flow layout to my collection view which is currently only setup to create a bunch of dummy cells.
How do I prevent movement in the horizontal axis and how do I ensure that the cells always come to a rest eventually
class SpringyColumnLayout: UICollectionViewFlowLayout {
private lazy var dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self)
override func prepare() {
super.prepare()
guard let cv = collectionView else { return }
let availableWidth = cv.bounds.inset(by: cv.layoutMargins).width
let minColumnWidth: CGFloat = 300
let maxNumberOfColumns = Int(availableWidth / minColumnWidth)
let cellWidth = (availableWidth / CGFloat(maxNumberOfColumns))
.rounded(.down)
self.itemSize = CGSize(width: cellWidth, height: 70)
self.sectionInset = UIEdgeInsets(
top: minimumInteritemSpacing,
left: 0,
bottom: 0,
right: 0
)
self.sectionInsetReference = .fromSafeArea
if dynamicAnimator.behaviors.isEmpty {
let contentSize = collectionViewContentSize
let contentBounds = CGRect(origin: .zero, size: contentSize)
guard let items = super.layoutAttributesForElements(in: contentBounds)
else { return }
for item in items {
let spring = UIAttachmentBehavior(
item: item,
attachedToAnchor: item.center
)
spring.length = 0
spring.damping = 0.8
spring.frequency = 1
self.dynamicAnimator.addBehavior(spring)
}
}
}
override func layoutAttributesForElements(
in rect: CGRect
) -> [UICollectionViewLayoutAttributes]? {
return dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]
}
override func layoutAttributesForItem(
at indexPath: IndexPath
) -> UICollectionViewLayoutAttributes? {
return dynamicAnimator.layoutAttributesForCell(at: indexPath)
}
override func shouldInvalidateLayout(
forBoundsChange newBounds: CGRect
) -> Bool {
let scrollView = self.collectionView!
let scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y
let touchLocation = scrollView.panGestureRecognizer
.location(in: scrollView)
for case let spring as UIAttachmentBehavior in dynamicAnimator.behaviors {
let anchorPoint = spring.anchorPoint
let yDistanceFromTouch = abs(touchLocation.y - anchorPoint.y)
let xDistanceFromTouch = abs(touchLocation.x - anchorPoint.x)
let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500
let item = spring.items.first!
var center = item.center
if scrollDelta < 0 {
center.y += max(scrollDelta, scrollDelta * scrollResistance)
} else {
center.y += min(scrollDelta, scrollDelta * scrollResistance)
}
item.center = center
dynamicAnimator.updateItem(usingCurrentState: item)
}
return false
}
}

Carousel Layout Collection View Issue

Hello, I have followed raywenderlich video for collection view custom
layout. UICollectionViewFlowLayout is subclassed as below
import UIKit
class CharacterFlowLayout: UICollectionViewFlowLayout {
let standardItemAlpha: CGFloat = 0.5
let standardItemScale: CGFloat = 0.5
var count = 0
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var attributesCopy = [UICollectionViewLayoutAttributes]()
for itemAttributes in attributes! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
changeLayoutAttributes(itemAttributesCopy)
attributesCopy.append(itemAttributesCopy)
}
return attributesCopy
}
**override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool** {
return true
}
**func changeLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes)** {
let collectionCenter = collectionView!.frame.size.height/2
let offset = collectionView!.contentOffset.y // bounds
let normalizedCenter = attributes.center.y - offset
let maxDistance = self.itemSize.height + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.standardItemAlpha) + self.standardItemAlpha
let scale = ratio * (1 - self.standardItemScale) + self.standardItemScale
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
} }
I don't understand the logic behind the calculation? Could you please explain why we are calculating distance and ratio?

Animate Collection View Bounds Change with Custom Layout Animation

I have a collection view and I want it to have 2 states: collapsed and expanded.
Here are the two states:
The collection view has a red background, the cells have a purple background, and the containing view of the collection view has a gray background.
I am using a custom subclass of UICollectionViewFlowLayout that handles paging, scaling, and opacity of the cells. The layout is supposed to assure that one of the cells is always centered.
What I'm trying to achieve
A smooth animation between the two states where the cells simply grow/shrink with the collection view.
What I'm experiencing
Since the width of the cell changes between the two states, I'm seeing not only the default fade animation between the cells, but also the cell that was centered in one state is no longer centered.
What I tried
I tried following the UPCarouselLayout for the initial layout. I tried following this objc.io post for animation ideas and specifically the section on device rotations. I wasn't able to use their approach because the frame returned by layoutAttributesForElement(at indexPath: IndexPath) was incorrect.
The layout code
protocol TrayCarouselFlowLayoutDelegate: class {
func sizeForItemInCarousel(_ collectionView: UICollectionView, _ layout: TrayCarouselFlowLayout) -> CGSize
}
class TrayCarouselFlowLayout: UICollectionViewFlowLayout {
fileprivate var collectionViewSize: CGSize = .zero
fileprivate var peekItemScale: CGFloat = 0.95
fileprivate var peekItemAlpha: CGFloat = 0.65
fileprivate var peekItemShift: CGFloat = 0.0
var spacing = 16
weak var delegate: TrayCarouselFlowLayoutDelegate!
fileprivate var indexPathsToAnimate: [IndexPath] = []
override func prepare() {
super.prepare()
guard let size = collectionView?.bounds.size, collectionViewSize != size else { return }
setUpCollectionView()
updateLayout()
collectionViewSize = size
}
fileprivate func setUpCollectionView() {
guard let collectionView = collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}
fileprivate func updateLayout() {
guard let collectionView = collectionView else { return }
itemSize = delegate.sizeForItemInCarousel(collectionView, self)
let yInset = (collectionView.bounds.height - itemSize.height) / 2
let xInset = (collectionView.bounds.width - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset)
let side = itemSize.width
let scaledItemOffset = (side - side * peekItemScale) / 2
minimumLineSpacing = spacing - scaledItemOffset
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let oldBounds = collectionView?.bounds ?? .zero
if oldBounds != newBounds {
return true
}
return false
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map { self.transformLayoutAttributes($0) }
}
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = collectionView else { return attributes }
let collectionCenter = collectionView.frame.size.width / 2
let offset = collectionView.contentOffset.x
let normalizedCenter = attributes.center.x - offset
let maxDistance = itemSize.width + minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance) / maxDistance
let alpha = ratio * (1 - peekItemAlpha) + peekItemAlpha
let scale = ratio * (1 - peekItemScale) + peekItemScale
let shift = (1 - ratio) * peekItemShift
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
attributes.center.y += shift
return attributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView, !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let midSide: CGFloat = collectionViewSize.width / 2
let proposedContentOffsetCenterOrigin = proposedContentOffset.x + midSide
let closest = layoutAttributes
.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }
.first ?? UICollectionViewLayoutAttributes()
let targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
return targetContentOffset
}
}
What I need help with
My goal is to simply grow the collection view and the cells along with it. Since I'm changing the section insets and the size of the cell in my code, the most challenging part is keeping the cell centered during the growth/shrinking. Any help would be greatly appreciated!
EDIT: If I don't change the width of the cell when expanding/collapsing, this behaves ALMOST how I want it. There is still a fade animation but that would be acceptable if it was the only part off the mark. For some reason, redefining the width while the layout cells are growing is what is causing the issue seen in the GIF.
Here is a GIF where the width is static:

Resources