Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/ios/106.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
iOS卡刷卡动画&;滚动视图_Ios_Iphone - Fatal编程技术网

iOS卡刷卡动画&;滚动视图

iOS卡刷卡动画&;滚动视图,ios,iphone,Ios,Iphone,我正在实现一个iPhone应用程序,我正在尝试构建一个功能,它是刷卡(和Tinder一样)和滚动效果的组合。请查看以下详细信息 当用户打开应用程序时,它会显示其他用户配置文件。因此,登录用户可以向左(nope)或向右(类似)刷卡,该卡将从列表中删除,并显示下一张用户卡。现在,如果用户不想刷卡,那么他可以向上或向下滚动查看其他用户配置文件 因此,是否可以实现刷卡和滚动功能的组合。您需要实现刷卡手势和滚动视图。在少数情况下可能会很棘手,设置以下条件会有所帮助 scrollView.panGestur

我正在实现一个iPhone应用程序,我正在尝试构建一个功能,它是刷卡(和Tinder一样)和滚动效果的组合。请查看以下详细信息

当用户打开应用程序时,它会显示其他用户配置文件。因此,登录用户可以向左(nope)或向右(类似)刷卡,该卡将从列表中删除,并显示下一张用户卡。现在,如果用户不想刷卡,那么他可以向上或向下滚动查看其他用户配置文件


因此,是否可以实现刷卡和滚动功能的组合。

您需要实现刷卡手势和滚动视图。在少数情况下可能会很棘手,设置以下条件会有所帮助

scrollView.panGestureRecognizer.requireGestureRecognizerToFail(< UISwipeGestureRecognizer instance>)
scrollView.PangestureRecognitzer.RequireGestureRecognitzerToFail(
请参阅以下内容以更好地理解:


我知道这个问题现在有点老了,但是是的,这正是我过去几个月一直在构建的

我基本上添加了一个
uipangestureerecognizer
到一个
UICollectionView
中,其中包含一个自定义的流程布局(用于卡片分页等)

这就是它看起来的样子:

主要部件逻辑:

import Foundation

/**
 The VerticalCardSwiper is a subclass of `UIView` that has a `VerticalCardSwiperView` embedded.

 To use this, you need to implement the `VerticalCardSwiperDatasource`.

 If you want to handle actions like cards being swiped away, implement the `VerticalCardSwiperDelegate`.
 */
public class VerticalCardSwiper: UIView {

    /// The collectionView where all the magic happens.
    public var verticalCardSwiperView: VerticalCardSwiperView!

    /// Indicates if side swiping on cards is enabled. Default value is `true`.
    @IBInspectable public var isSideSwipingEnabled: Bool = true
    /// The inset (spacing) at the top for the cards. Default is 40.
    @IBInspectable public var topInset: CGFloat = 40 {
        didSet {
            setCardSwiperInsets()
        }
    }
    /// The inset (spacing) at each side of the cards. Default is 20.
    @IBInspectable public var sideInset: CGFloat = 20 {
        didSet {
            setCardSwiperInsets()
        }
    }
    /// Sets how much of the next card should be visible. Default is 50.
    @IBInspectable public var visibleNextCardHeight: CGFloat = 50 {
        didSet {
            setCardSwiperInsets()
        }
    }
    /// Vertical spacing between CardCells. Default is 40.
    @IBInspectable public var cardSpacing: CGFloat = 40 {
        willSet {
            flowLayout.minimumLineSpacing = newValue
        }
    }

    /// The transform animation that is shown on the top card when scrolling through the cards. Default is 0.05.
    @IBInspectable public var firstItemTransform: CGFloat = 0.05 {
        willSet {
            flowLayout.firstItemTransform = newValue
        }
    }

    public weak var delegate: VerticalCardSwiperDelegate?
    public weak var datasource: VerticalCardSwiperDatasource? {
        didSet{
            numberOfCards = datasource?.numberOfCards(verticalCardSwiperView: self.verticalCardSwiperView) ?? 0
        }
    }

    /// The amount of cards in the collectionView.
    fileprivate var numberOfCards: Int = 0
    /// We use this horizontalPangestureRecognizer for the vertical panning.
    fileprivate var horizontalPangestureRecognizer: UIPanGestureRecognizer!
    /// Stores a `CGRect` with the area that is swipeable to the user.
    fileprivate var swipeAbleArea: CGRect!
    /// The `CardCell` that the user can (and is) moving.
    fileprivate var swipedCard: CardCell! {
        didSet {
            setupCardSwipeDelegate()
        }
    }

    /// The flowlayout used in the collectionView.
    fileprivate lazy var flowLayout: VerticalCardSwiperFlowLayout = {
        let flowLayout = VerticalCardSwiperFlowLayout()
        flowLayout.firstItemTransform = firstItemTransform
        flowLayout.minimumLineSpacing = cardSpacing
        flowLayout.isPagingEnabled = true
        return flowLayout
    }()

    public override init(frame: CGRect) {
        super.init(frame: frame)

        commonInit()
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        commonInit()
    }

    fileprivate func commonInit() {

        setupVerticalCardSwiperView()
        setupConstraints()
        setCardSwiperInsets()
        setupGestureRecognizer()
    }
}

extension VerticalCardSwiper: CardDelegate {

    internal func willSwipeAway(cell: CardCell, swipeDirection: SwipeDirection) {

        verticalCardSwiperView.isUserInteractionEnabled = false

        if let index = verticalCardSwiperView.indexPath(for: cell)?.row {
            self.delegate?.willSwipeCardAway?(card: cell, index: index, swipeDirection: swipeDirection)
        }
    }

    internal func didSwipeAway(cell: CardCell, swipeDirection direction: SwipeDirection) {

        if let indexPathToRemove = verticalCardSwiperView.indexPath(for: cell){

            self.numberOfCards -= 1
            swipedCard = nil

            self.verticalCardSwiperView.performBatchUpdates({
                self.verticalCardSwiperView.deleteItems(at: [indexPathToRemove])
            }) { [weak self] (finished) in
                if finished {
                    self?.verticalCardSwiperView.collectionViewLayout.invalidateLayout()
                    self?.verticalCardSwiperView.isUserInteractionEnabled = true
                    self?.delegate?.didSwipeCardAway?(card: cell, index: indexPathToRemove.row ,swipeDirection: direction)
                }
            }
        }
    }

    internal func didDragCard(cell: CardCell, swipeDirection: SwipeDirection) {

        if let index = verticalCardSwiperView.indexPath(for: cell)?.row {
            self.delegate?.didDragCard?(card: cell, index: index, swipeDirection: swipeDirection)
        }
    }

    fileprivate func setupCardSwipeDelegate() {
        swipedCard?.delegate = self
    }
}

extension VerticalCardSwiper: UIGestureRecognizerDelegate {

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

        if let panGestureRec = gestureRecognizer as? UIPanGestureRecognizer {

            // When a horizontal pan is detected, we make sure to disable the collectionView.panGestureRecognizer so that it doesn't interfere with the sideswipe.
            if panGestureRec == horizontalPangestureRecognizer, panGestureRec.direction!.isX {
                return false
            }
        }
        return true
    }

    /// We set up the `horizontalPangestureRecognizer` and attach it to the `collectionView`.
    fileprivate func setupGestureRecognizer(){

        horizontalPangestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        horizontalPangestureRecognizer.maximumNumberOfTouches = 1
        horizontalPangestureRecognizer.delegate = self
        verticalCardSwiperView.addGestureRecognizer(horizontalPangestureRecognizer)
        verticalCardSwiperView.panGestureRecognizer.maximumNumberOfTouches = 1
    }

    /**
     This function is called when a pan is detected inside the `collectionView`.
     We also take care of detecting if the pan gesture is inside the `swipeAbleArea` and we animate the cell if necessary.
     - parameter sender: The `UIPanGestureRecognizer` that detects the pan gesture. In this case `horizontalPangestureRecognizer`.
     */
    @objc fileprivate func handlePan(sender: UIPanGestureRecognizer){

        guard isSideSwipingEnabled else {
            return
        }

        /// The taplocation relative to the superview.
        let location = sender.location(in: self)
        /// The taplocation relative to the collectionView.
        let locationInCollectionView = sender.location(in: verticalCardSwiperView)
        /// The translation of the finger performing the PanGesture.
        let translation = sender.translation(in: self)

        if swipeAbleArea.contains(location) && !verticalCardSwiperView.isScrolling {
            if let swipedCardIndex = verticalCardSwiperView.indexPathForItem(at: locationInCollectionView) {
                /// The card that is swipeable inside the SwipeAbleArea.
                swipedCard = verticalCardSwiperView.cellForItem(at: swipedCardIndex) as? CardCell
            }
        }

        if swipedCard != nil && !verticalCardSwiperView.isScrolling {

            /// The angle we pass for the swipe animation.
            let maximumRotation: CGFloat = 1.0
            let rotationStrength = min(translation.x/swipedCard.frame.width, maximumRotation)
            let angle = (CGFloat.pi/10.0) * rotationStrength

            switch (sender.state) {

            case .began:
                break

            case .changed:
                swipedCard.animateCard(angle: angle, horizontalTranslation: translation.x)
                break

            case .ended:
                swipedCard.endedPanAnimation(angle: angle)
                swipedCard = nil
                break

            default:
                swipedCard.resetToCenterPosition()
                swipedCard = nil
            }
        }
    }
}

extension VerticalCardSwiper: UICollectionViewDelegate, UICollectionViewDataSource {

    /**
     Reloads all of the data for the VerticalCardSwiperView.

     Call this method sparingly when you need to reload all of the items in the VerticalCardSwiper. This causes the VerticalCardSwiperView to discard any currently visible items (including placeholders) and recreate items based on the current state of the data source object. For efficiency, the VerticalCardSwiperView only displays those cells and supplementary views that are visible. If the data shrinks as a result of the reload, the VerticalCardSwiperView adjusts its scrolling offsets accordingly.
    */
    public func reloadData(){
        verticalCardSwiperView.reloadData()
    }

    /**
     Register a class for use in creating new CardCells.
     Prior to calling the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view,
     you must use this method or the register(_:forCellWithReuseIdentifier:) method
     to tell the collection view how to create a new cell of the given type.
     If a cell of the specified type is not currently in a reuse queue,
     the VerticalCardSwiper uses the provided information to create a new cell object automatically.
     If you previously registered a class or nib file with the same reuse identifier,
     the class you specify in the cellClass parameter replaces the old entry.
     You may specify nil for cellClass if you want to unregister the class from the specified reuse identifier.
     - parameter cellClass: The class of a cell that you want to use in the VerticalCardSwiper
     identifier
     - parameter identifier: The reuse identifier to associate with the specified class. This parameter must not be nil and must not be an empty string.
     */
    public func register(_ cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String) {
        verticalCardSwiperView.register(cellClass, forCellWithReuseIdentifier: identifier)
    }

    /**
     Register a nib file for use in creating new collection view cells.
     Prior to calling the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view,
     you must use this method or the register(_:forCellWithReuseIdentifier:) method
     to tell the collection view how to create a new cell of the given type.
     If a cell of the specified type is not currently in a reuse queue,
     the collection view uses the provided information to create a new cell object automatically.
     If you previously registered a class or nib file with the same reuse identifier,
     the object you specify in the nib parameter replaces the old entry.
     You may specify nil for nib if you want to unregister the nib file from the specified reuse identifier.
     - parameter nib: The nib object containing the cell object. The nib file must contain only one top-level object and that object must be of the type UICollectionViewCell.
     identifier
     - parameter identifier: The reuse identifier to associate with the specified nib file. This parameter must not be nil and must not be an empty string.
    */
    public func register(nib: UINib?, forCellWithReuseIdentifier identifier: String) {
        verticalCardSwiperView.register(nib, forCellWithReuseIdentifier: identifier)
    }

    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        return self.numberOfCards
    }

    public func numberOfSections(in collectionView: UICollectionView) -> Int {

        return 1
    }

    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        return (datasource?.cardForItemAt(verticalCardSwiperView: verticalCardSwiperView, cardForItemAt: indexPath.row))!
    }

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        delegate?.didScroll?(verticalCardSwiperView: verticalCardSwiperView)
    }

    fileprivate func setupVerticalCardSwiperView(){

        verticalCardSwiperView = VerticalCardSwiperView(frame: self.frame, collectionViewLayout: flowLayout)
        verticalCardSwiperView.decelerationRate = UIScrollViewDecelerationRateFast
        verticalCardSwiperView.backgroundColor = UIColor.clear
        verticalCardSwiperView.showsVerticalScrollIndicator = false
        verticalCardSwiperView.delegate = self
        verticalCardSwiperView.dataSource = self

        self.numberOfCards = datasource?.numberOfCards(verticalCardSwiperView: verticalCardSwiperView) ?? 0

        self.addSubview(verticalCardSwiperView)
    }

    fileprivate func setupConstraints(){

        verticalCardSwiperView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            verticalCardSwiperView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            verticalCardSwiperView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            verticalCardSwiperView.topAnchor.constraint(equalTo: self.topAnchor),
            verticalCardSwiperView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
            ])
    }

    fileprivate func setCardSwiperInsets(){

        verticalCardSwiperView.contentInset = UIEdgeInsets(top: topInset, left: sideInset, bottom: topInset + flowLayout.minimumLineSpacing + visibleNextCardHeight, right: sideInset)
    }
}

extension VerticalCardSwiper: UICollectionViewDelegateFlowLayout {

    public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let itemSize = calculateItemSize(for: indexPath.row)

        // set cellHeight in the custom flowlayout, we use this for paging calculations.
        flowLayout.cellHeight = itemSize.height

        if swipeAbleArea == nil {
            // Calculate and set the swipeAbleArea. We use this to determine wheter the cell can be swiped to the sides or not.
            let swipeAbleAreaOriginY = collectionView.frame.origin.y + collectionView.contentInset.top
            swipeAbleArea = CGRect(x: 0, y: swipeAbleAreaOriginY, width: self.frame.width, height: itemSize.height)
        }
        return itemSize
    }

    fileprivate func calculateItemSize(for index: Int) -> CGSize {

        let cellWidth: CGFloat!
        let cellHeight: CGFloat!
        let xInsets = sideInset * 2
        let yInsets = cardSpacing + visibleNextCardHeight + topInset

        // get size from delegate if the sizeForItem function is called.
        if let customSize = delegate?.sizeForItem?(verticalCardSwiperView: verticalCardSwiperView, index: index) {
            // set custom sizes and make sure sizes are not negative, if they are, don't subtract the insets.
            cellWidth = customSize.width - (customSize.width - xInsets > 0 ? xInsets : 0)
            cellHeight = customSize.height - (customSize.height - yInsets > 0 ? yInsets : 0)
        } else {
            cellWidth = verticalCardSwiperView.frame.size.width - xInsets
            cellHeight = verticalCardSwiperView.frame.size.height - yInsets
        }
        return CGSize(width: cellWidth, height: cellHeight)
    }
}
自定义流程布局:

import UIKit

/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {

    /// This property sets the amount of scaling for the first item.
    internal var firstItemTransform: CGFloat?
    /// This property enables paging per card. The default value is true.
    internal var isPagingEnabled: Bool = true
    /// Stores the height of a CardCell.
    internal var cellHeight: CGFloat!

    internal override func prepare() {
        super.prepare()

        assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
        assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
    }

    internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let items = NSArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)

        items.enumerateObjects(using: { (object, index, stop) -> Void in
            let attributes = object as! UICollectionViewLayoutAttributes

            self.updateCellAttributes(attributes)
        })
        return items as? [UICollectionViewLayoutAttributes]
    }

    // We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
    internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

    // Cell paging
    internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        // If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
        guard isPagingEnabled else {
            let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
            return latestOffset
        }

        // Page height used for estimating and calculating paging.
        let pageHeight = cellHeight + self.minimumLineSpacing

        // Make an estimation of the current page position.
        let approximatePage = self.collectionView!.contentOffset.y/pageHeight

        // Determine the current page based on velocity.
        let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)

        // Create custom flickVelocity.
        let flickVelocity = velocity.y * 0.3

        // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
        let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

        // Calculate newVerticalOffset.
        let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top

        return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
    }

    internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {

        // make sure the zIndex of the next card is higher than the one we're swiping away.
        let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
        let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
        nextAttr?.zIndex = nextIndexPath.row

        // attributes for swiping card away
        let attr = self.layoutAttributesForItem(at: itemIndexPath)

        return attr
    }

    /**
     Updates the attributes.
     Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
     - parameter attributes: The attributes we're updating.
     */
    fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
        let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
        let maxY = attributes.frame.origin.y

        let finalY = max(minY, maxY)
        var origin = attributes.frame.origin
        let deltaY = (finalY - origin.y) / attributes.frame.height

        if let itemTransform = firstItemTransform {
            let scale = 1 - deltaY * itemTransform
            attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
            // TODO: add card stack effect (like Shazam)
        }
        origin.y = finalY
        attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
        attributes.zIndex = attributes.indexPath.row
    }
}
导入UIKit
///自定义“UICollectionViewFlowLayout”,提供诸如分页和“CardCell”移动等flowlayout信息。
内部类VerticalCardSwiperFlowLayout:UICollectionViewFlowLayout{
///此属性设置第一项的缩放量。
内部var firstItemTransform:CGFloat?
///此属性启用每张卡的分页。默认值为true。
内部变量isPaginEnabled:Bool=true
///存储CardCell的高度。
内部变量单元格高度:CGFloat!
内部重写函数准备(){
超级准备
断言(collectionView!.numberOfSections==1,“节数应始终为1。”)
断言(collectionView!.isPaginEnabled==false,“不应启用collectionView本身的分页。若要启用单元格分页,请改用VerticalCardSwiperFlowLayout的isPaginEnabled属性。”)
}
内部重写函数layoutAttributesForElements(在rect:CGRect中)->[UICollectionViewLayoutAttributes]{
let items=NSArray(数组:super.layouttributesforements(in:rect)!,copyItems:true)
枚举对象(使用:{(对象,索引,停止)->Void in
让attributes=对象为!UICollectionViewLayoutAttributes
self.updateCellAttributes(属性)
})
将项目返回为?[UICollectionViewLayoutAttribute]
}
//当发生“边界更改”时,例如当我们缩放顶部单元格时,我们会使布局无效。这会强制对flowlayout进行布局更新。
内部覆盖函数应验证布局(对于边界新边界:CGRect)->Bool{
返回真值
}
//小区寻呼
内部覆盖函数targetContentOffset(forProposedContentOffset proposedContentOffset:CGPoint,带CrollingVelocity:CGPoint)->CGPoint{
//如果属性'isPaginEnabled'设置为false,则不启用分页,因此返回当前contentoffset。
卫兵被派往其他地方{
设latestOffset=super.targetContentOffset(forProposedContentOffset:proposedContentOffset,带CrollingVelocity:velocity)
返回延迟偏移
}
//用于估计和计算分页的页面高度。
让pageHeight=单元格高度+自身最小行间距
//估计当前页面位置。
让approximatePage=self.collectionView!.contentOffset.y/pageHeight
//根据速度确定当前页面。
让currentPage=(速度y<0.0)?地板(近似页面):天花板(近似页面)
//创建自定义flickVelocity。
设flickVelocity=velocity.y*0.3
//检查用户翻动了多少页,如果是真棒兄弟的话
import UIKit

/**
 The CardCell that the user can swipe away. Based on `UICollectionViewCell`.

 The cells will be recycled by the `VerticalCardSwiper`,
 so don't forget to override `prepareForReuse` when needed.
 */
@objc open class CardCell: UICollectionViewCell {

    internal weak var delegate: CardDelegate?

    open override func layoutSubviews() {

        self.layer.shouldRasterize = true
        self.layer.rasterizationScale = UIScreen.main.scale

        super.layoutSubviews()
    }

    open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)

        self.layer.zPosition = CGFloat(layoutAttributes.zIndex)
    }

    open override func prepareForReuse() {
        super.prepareForReuse()

        // need to unhide a cell for reuse (cell is hidden when swiped away)
        self.isHidden = false
    }

    /**
     This function animates the card. The animation consists of a rotation and translation.
     - parameter angle: The angle the card rotates while animating.
     - parameter horizontalTranslation: The horizontal translation the card animates in.
     */
    public func animateCard(angle: CGFloat, horizontalTranslation: CGFloat){

        delegate?.didDragCard(cell: self, swipeDirection: determineCardSwipeDirection())

        var transform = CATransform3DIdentity
        transform = CATransform3DRotate(transform, angle, 0, 0, 1)
        transform = CATransform3DTranslate(transform, horizontalTranslation, 0, 1)

        self.layer.transform = transform
    }

    /**
     Resets the CardCell back to the center of the VerticalCardSwiperView.
     */
    public func resetToCenterPosition(){

        let cardCenterX = self.frame.midX
        let centerX = self.bounds.midX
        let initialSpringVelocity = fabs(cardCenterX - centerX)/100

        UIView.animate(withDuration: 0.5,
                       delay: 0,
                       usingSpringWithDamping: 0.6,
                       initialSpringVelocity: initialSpringVelocity,
                       options: .allowUserInteraction,
                       animations: { [weak self] in
                        self?.layer.transform = CATransform3DIdentity
        })
    }

    /**
     Called when the pan gesture is ended.
     Handles what happens when the user stops swiping a card.
     If a certain treshold of the screen is swiped, the `animateOffScreen` function is called,
     if the threshold is not reached, the card will be reset to the center by calling `resetToCenterPosition`.
     - parameter angle: The angle of the animation, depends on the direction of the swipe.
     */
    internal func endedPanAnimation(angle: CGFloat){

        let swipePercentageMargin = self.bounds.width * 0.4
        let cardCenterX = self.frame.midX
        let centerX = self.bounds.midX

        // check for left or right swipe and if swipePercentageMargin is reached or not
        if (cardCenterX < centerX - swipePercentageMargin || cardCenterX > centerX + swipePercentageMargin){
            animateOffScreen(angle: angle)
        } else {
            self.resetToCenterPosition()
        }
    }

    /**
     Animates to card off the screen and calls the `willSwipeAway` and `didSwipeAway` functions from the `CardDelegate`.
     - parameter angle: The angle that the card will rotate in (depends on direction). Positive means the card is swiped to the right, a negative angle means the card is swiped to the left.
     */
    fileprivate func animateOffScreen(angle: CGFloat){

        var transform = CATransform3DIdentity
        let direction = determineCardSwipeDirection()

        transform = CATransform3DRotate(transform, angle, 0, 0, 1)

        switch direction {

        case .Left:

            transform = CATransform3DTranslate(transform, -(self.frame.width * 2), 0, 1)
            break

        case .Right:

            transform = CATransform3DTranslate(transform, (self.frame.width * 2), 0, 1)
            break

        default: break

        }

        self.delegate?.willSwipeAway(cell: self, swipeDirection: direction)

        UIView.animate(withDuration: 0.2, animations: { [weak self] in
            self?.layer.transform = transform
        }){ (completed) in
            self.isHidden = true
            self.delegate?.didSwipeAway(cell: self, swipeDirection: direction)
        }
    }

    fileprivate func determineCardSwipeDirection() -> SwipeDirection {

        let cardCenterX = self.frame.midX
        let centerX = self.bounds.midX

        if cardCenterX < centerX {
            return .Left
        } else if cardCenterX > centerX {
            return .Right
        } else {
            return .None
        }
    }
}