ios - 向左滑动创建一个新的 collectionViewCell

标签 ios uicollectionview uicollectionviewcell uiswipegesturerecognizer

我正在尝试实现一种更流畅的方式来为 myCollectionView 添加新的 collectionViewCells(一次只显示一个单元格)。我希望它类似于当用户向左滑动时,如果用户在最后一个单元格上,myCollectionView 在用户滑动时插入一个新单元格,以便用户向左滑动“进入”单元格。而且我一次只允许用户滚动一个单元格。

编辑: 所以我觉得用语言来描述它有点困难,所以这里有一个 gif 来展示我的意思

enter image description here

所以在过去的几周里,我一直在尝试以多种不同的方式实现它,我发现最成功的一种是使用 scrollViewWillEndDragging 委托(delegate)方法,我已经这样实现了:

 func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    // Getting the size of the cells
    let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
    let cellWidth = flowLayout.itemSize.width
    let cellPadding = 10.0 as! CGFloat

    // Calculating which page "card" we should be on
    let currentOffset = scrollView.contentOffset.x - cellWidth/2
    print("currentOffset is \(currentOffset)")
    let cardWidth = cellWidth + cellPadding
    var page = Int(round((currentOffset)/(cardWidth) + 1))

    print("current page number is: \(page)")
    if (velocity.x < 0) {
        page -= 1

    }
    if (velocity.x > 0) {
        page += 1
    }
    print("Updated page number is: \(page)")
    print("Previous page number is: \(self.previousPage)")

    // Only allowing the user to scroll for one page!
    if(page > self.previousPage) {
        page = self.previousPage + 1
        self.previousPage = page

    }
    else if (page == self.previousPage) {
        page = self.previousPage
    }
    else {
        page = self.previousPage - 1
        self.previousPage = page
    }

    print("new page number is: " + String(page))
    print("addedCards.count + 1 is: " + String(addedCards.count + 1))
    if (page == addedCards.count) {
        print("reloading data")

        // Update data source
        addedCards.append("card")

        // Method 1
        cardCollectionView.reloadData()

        // Method 2
        //            let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
        //            cardCollectionView.insertItemsAtIndexPaths([newIndexPath])

    }
    //        print("Centering on new cell")
    // Center the cardCollectionView on the new page
    let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
    print("newOffset is: \(newOffset)")
    targetContentOffset.memory.x = newOffset

}

虽然我认为我几乎得到了预期的结果,但仍然存在一些问题和我发现的错误。

我主要担心的是,我没有在 myCollectionView 的末尾插入单个单元格,而是重新加载了整个表格。我这样做的原因是,如果我不这样做,那么 myCollectionView.contentOffset 将不会更改,因此在创建新单元格时,myCollectionView 是' t 以新创建的单元格为中心。

1. 如果用户滚动非常缓慢然后停止,则会创建新的单元格,但随后 myCollectionView 会卡在两个单元格之间,它不会居中新创建的单元格。

2.myCollectionView 由于 1. 而位于倒数第二个单元格和最后一个单元格之间时,下一次用户向右滑动,而不是创建一个单元格,而是创建两个单元格。

我还使用了不同的方法来实现此行为,例如使用 scrollViewDidScroll 和其他各种方法,但均无济于事。任何人都可以指出我正确的方向,因为我有点迷路了。

如果您想查看交互,可以通过以下链接下载我的项目: My Example Project

如果您有兴趣,可以使用这些旧方法:

为此我尝试了两种方法,

第一个是:

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, 
    targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        // page is the current cell that the user is on 
        // addedCards is the array that the data source works with
        if (page == addedCards.count + 1) {
            let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
            addedCards.append(placeholderFlashCardProxy)   
            let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
            cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
            cardCollectionView.reloadData()

        }
}

使用这种方法的问题在于:

  1. 有时我会因为以下原因而崩溃:NSInternalInconsistencyException',原因:'无效更新:第 0 节中的项目数量无效。
  2. 当添加一个新的 collectionCell 时,它有时会显示用户从前一个单元格写入的输入(这有时可能与单元格的出队和重用有关,尽管再次,我不确定,如果有人回答我将不胜感激)
  3. 插入不顺利,我希望用户能够在最后一个单元格向左滑动并“进入”一个新单元格。就像我目前在最后一个单元格上一样,向左滑动会自动将我置于新单元格中,因为现在当我向左滑动时会创建一个新单元格,它不会以新创建的单元格为中心

我使用的第二种方法是:

let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self

虽然滑动手势很少响应,但如果我使用点击手势,myCollectionView 总是响应,这很奇怪(我再次知道这本身就是一个问题)

我的问题是哪种方法更好地实现我上面描述的内容?如果没有一个是好的,我应该用什么来创造想要的结果,我已经尝试了两天了,我想知道是否有人可以指出我正确的方向。谢谢!

最佳答案

我希望这能以某种方式帮助您 :)

更新

我更新了代码以解决向任一方向滚动的问题。 可以在此处找到更新的要点

New Updated Gist

Old Gist

首先我要为卡片定义一些模型

class Card {
    var someCardData : String?
}

接下来,创建一个 Collection View 单元格,里面有一个卡片 View ,我们将对其应用转换

class CollectionViewCell : UICollectionViewCell {

     override init(frame: CGRect) {
         super.init(frame: frame)
         self.addSubview(cardView)
         self.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
     }()
}

接下来使用 Collection View 设置 View Controller 。正如您将看到的,有一个 CustomCollectionView 类,我将在最后定义它。

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

    var cards = [Card(), Card()]

    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 collectionView : CollectionView = {
        [unowned self] in

        // MARK: Custom Flow Layout defined below
        var layout = CustomCollectionViewFlowLayout()
        layout.contentDelegate = self

        var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
        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
        cell.contentView.backgroundColor = UIColor.blueColor()

        // UPDATE If the cell is not the initial index, 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)
        }
        return cell
    }
}

已更新 - 现在是真正棘手的部分。我要定义 CustomCollectionViewFlowLayout。协议(protocol)回调返回流布局计算的下一个插入索引

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
    }
}

并在您的 View Controller 中定义委托(delegate)方法,以便在委托(delegate)告诉它需要新索引时插入新卡片

extension ViewController : CollectionViewFlowLayoutDelegate {
    func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
        cards.append(Card())
        collectionView.performBatchUpdates({
            self.collectionView.insertItemsAtIndexPaths([index])
        }) { (complete) in

    }
}

下面是自定义的 Collection View ,它在相应地滚动时应用动画 :)

class CollectionView : UICollectionView {

    override var contentOffset: CGPoint {
        didSet {
            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)
        }
    }
}

关于ios - 向左滑动创建一个新的 collectionViewCell,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36348464/

相关文章:

ios - 批量更新期间重复的单元格动画

ios - 如何创建更改特定 uicollectionviewcell 属性的操作?

iphone - 查找 GPS 信号强度

iOS 和 Swift : Ok to force unwrap tableView. 索引路径?

ios - 我需要调用 UICollectionView 的 dequeueCell : from within the data source's cellForPath:?

ios - 重用单元格和滚动的 UICollectionView 问题

ios - 如何随着内容大小的增加动态增加 UICollectionView 高度?

ios - 如何在 iOS 中以编程方式将图像传递给 View Controller ?

ios - iOS7 中的 MFMailComposeViewController 自定义

ios - 具有辅助功能的 UITableViewCell 中的 UICollectionView