Last active
September 1, 2023 10:15
-
-
Save AntonTheDev/5b47214628aee207a131c537f86b8bb8 to your computer and use it in GitHub Desktop.
StackOverflow Flip Card Animation New-Collectionviewcell
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
// | |
// ViewController.swift | |
// SwiftLayout | |
// | |
// Created by Anton Doudarev on 5/24/16. | |
// Copyright © 2016 Anton Doudarev. All rights reserved. | |
// | |
import UIKit | |
// Layout Constants | |
let tableViewHeight : CGFloat = 300.0 | |
let contentPadding : CGFloat = 20.0 | |
// Model to represent some sort of Card | |
class Card { | |
var someCardData : String? | |
} | |
// Custom CollectionViewCell - Contains the cardview that we will perform a transform on | |
class CollectionViewCell : UICollectionViewCell { | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
contentView.backgroundColor = UIColor.blueColor() | |
contentView.alpha = 1.0 | |
addSubview(cardView) | |
addSubview(cardlabel) | |
} | |
override func prepareForReuse() { | |
super.prepareForReuse() | |
cardView.alpha = 1.0 | |
cardView.layer.transform = CATransform3DIdentity | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
cardView.frame = CGRectMake(contentPadding, | |
contentPadding, | |
contentView.bounds.width - (contentPadding * 2.0), | |
contentView.bounds.height - (contentPadding * 2.0)) | |
cardlabel.frame = cardView.frame | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
lazy var cardView : UIView = { | |
[unowned self] in | |
var view = UIView(frame: CGRectZero) | |
view.backgroundColor = UIColor.whiteColor() | |
return view | |
}() | |
lazy var cardlabel : UILabel = { | |
[unowned self] in | |
var label = UILabel(frame: CGRectZero) | |
label.backgroundColor = UIColor.whiteColor() | |
label.textAlignment = .Center | |
return label | |
}() | |
} | |
// The View Controller containing the collection view | |
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, CollectionViewFlowLayoutDelegate { | |
var cards = [Card(), Card()] | |
var lastInsertIndex : NSIndexPath? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.addSubview(collectionView) | |
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight) | |
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding) | |
} | |
lazy var flowLayout : CustomCollectionViewFlowLayout = { | |
[unowned self] in | |
var layout = CustomCollectionViewFlowLayout() | |
layout.contentDelegate = self | |
return layout | |
}() | |
lazy var collectionView : CollectionView = { | |
[unowned self] in | |
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : self.flowLayout) | |
collectionView.clipsToBounds = true | |
collectionView.showsVerticalScrollIndicator = false | |
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell") | |
collectionView.delegate = self | |
collectionView.dataSource = self | |
return collectionView | |
}() | |
// MARK: - UICollectionViewDelegate, UICollectionViewDataSource | |
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { | |
return 1 | |
} | |
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |
return cards.count | |
} | |
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize { | |
return CGSizeMake(collectionView.bounds.width, tableViewHeight) | |
} | |
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { | |
let cellIdentifier = "CollectionViewCell" | |
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell | |
// If the cell is not the initial intdex, and is equal the to animating index | |
// Prepare it's initial state | |
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{ | |
cell.cardView.alpha = 0.0 | |
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0) | |
} | |
cell.cardlabel.text = "\(indexPath.row)" | |
return cell | |
} | |
// MARK: - CollectionViewFlowLayoutDelegate | |
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) { | |
cards.append(Card()) | |
collectionView.performBatchUpdates({ | |
self.collectionView.insertItemsAtIndexPaths([index]) | |
}) { (complete) in | |
} | |
} | |
} | |
/** | |
* This protocol defines the the change in visible index when | |
* targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:) is called | |
*/ | |
protocol CollectionViewFlowLayoutDelegate : class { | |
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) | |
} | |
/** | |
* Custom FlowLayout | |
* Tracks the currently visible index and updates the proposed content offset | |
*/ | |
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { | |
weak var contentDelegate: CollectionViewFlowLayoutDelegate? | |
// Tracks the card to be animated | |
// TODO: - Adjusted if cards are deleted by one if cards are deleted | |
private var animatingIndex : Int = 0 | |
// Tracks thje currently visible index | |
private var visibleIndex : Int = 0 { | |
didSet { | |
if visibleIndex > oldValue { | |
if visibleIndex > animatingIndex { | |
// Only increment the animating index forward | |
animatingIndex = visibleIndex | |
} | |
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 { | |
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0) | |
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex) | |
} | |
} else if visibleIndex < oldValue && animatingIndex == oldValue { | |
// if we start panning to the left, and the animating index is the old value | |
// let set the animating index to the last card. | |
animatingIndex = oldValue + 1 | |
} | |
} | |
} | |
override init() { | |
super.init() | |
self.minimumInteritemSpacing = 0.0 | |
self.minimumLineSpacing = 0.0 | |
self.scrollDirection = .Horizontal | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
// The width offset threshold percentage from 0 - 1 | |
let thresholdOffsetPrecentage : CGFloat = 0.5 | |
// This is the flick velocity threshold | |
let velocityThreshold : CGFloat = 0.4 | |
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { | |
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5)) | |
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5)) | |
let currentHorizontalOffset = collectionView!.contentOffset.x | |
// If you either traverse far enought in either direction, | |
// or flicked the scrollview over the horizontal velocity in either direction, | |
// adjust the visible index accordingly | |
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold { | |
visibleIndex = max(0 , (visibleIndex - 1)) | |
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold { | |
visibleIndex += 1 | |
} | |
var _proposedContentOffset = proposedContentOffset | |
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex) | |
return _proposedContentOffset | |
} | |
} | |
/** | |
* Custom CollectionView | |
* Performs the magic | |
*/ | |
class CollectionView : UICollectionView { | |
override var contentOffset: CGPoint { | |
didSet { | |
if let flowlayout = self.collectionViewLayout as? CustomCollectionViewFlowLayout { | |
// Dont do anything is the visible index is not equal to | |
// the animating index | |
if flowlayout.animatingIndex != flowlayout.visibleIndex { | |
return | |
} else if self.tracking { | |
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go | |
// So you should be adjusting the second to last cell on the screen | |
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0)) | |
} else { | |
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls | |
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index | |
// by adding 1, thus we need to continue the trasition on the second the last cell | |
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0)) | |
} | |
} | |
} | |
} | |
/** | |
This method applies the transform accordingly to the cell at a specified index | |
- parameter atIndex: index of the cell to adjust | |
*/ | |
func adjustTransitionForOffset(atIndex : NSIndexPath) { | |
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell { | |
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width | |
lastCell.cardView.alpha = progress | |
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment