iOS7+ UICollectionView拖拽重排

UIcollectionView是開發中最常使用到的組件之一,然而其拖拽排序的功能直到iOS9才引入,iOS9以前的版本并沒有原生的支持。為此我寫了一個小型庫 SortableCollectionView,方便地集成拖拽排序的功能。項目地址https://github.com/luowenxing/SortableCollectionView 只需要直接使用或者子類化SortableCollectionView,并實現簡單的代理方法,就能方便地實現拖拽排序。同時支持自定義拖拽的視圖,比如對拖拽中的Cell進行放大、設置背景顏色等等。

效果展示

  • 普通的流式布局效果,仿支付寶主頁的排序,可以看到對拖拽的視圖做了一個放大1.2倍和設置背景為灰色兩個操作。


    demoFlow.gif
  • 瀑布流效果


    demoWaterfall.gif

使用

  • 在你的工程中引入SortableCollectionView.swift
  • 直接使用或者子類化SortableCollectionView,并在xib/storyboard中指定。如果是純代碼方式創建的,把awakeFromNib方法的代碼作為初始化代碼就行。這個問題之后會進行優化。
  • 像往常一樣實現UICollectionView的代理方法和布局類。
  • 實現SortableCollectionViewDelegate并設置sortableDelegate
@objc protocol SortableCollectionViewDelegate:NSObjectProtocol {
    // 排序開始,定制拖拽排序的視圖 
    optional func beginDragAndInitDragCell(collectionView:SortableCollectionView,dragCell:UIView)
    
    // 排序結束,重設排序視圖
    optional func endDragAndResetDragCell(collectionView:SortableCollectionView,dragCell:UIView)
    
    // 排序結束,對排序完成后的真實Cell進行操作,比如類似支付寶的首頁排序,對沒有移動的Cell顯示刪除按鈕
    optional func endDragAndOperateRealCell(collectionView:SortableCollectionView,realCell:UICollectionViewCell,isMoved:Bool)
    
    // 排序完成,交換數據源
    func exchangeDataSource(fromIndex:NSIndexPath,toIndex:NSIndexPath)
}
  • 推薦在你的UICollectionViewCell中實現NSCopying協議,這樣就可以更好地定制化拖拽的view。否則則會使用截圖方法snapshotViewAfterScreenUpdates作為拖拽的view,它只是一個單獨的view,沒有相應的view層次結構。
  • 如果是iOS9系統,并且你實現了
func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool 
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) 

則會使用系統原生的拖拽排序,目前原生排序是一個簡單的版本,不支持自定義的拖拽view。你可以在Demo里面試一試,比較兩種方式。

原理分析

  • 使用長按手勢識別UILongPressGestureRecognizer,這個方法可以監聽長按、移動、結束,符合我們的需求。
  • 長按后,拷貝或者截圖選中的Cell,加入到superview,因為這樣會方便處理之后的滾動。
  • 移動過程中調用moveItemAtIndexPath方法交換相應的Cell。
  • 比較難處理的地方就是滾動了。滾動原理其實比較簡單,就是使用NSTimer并加入到RunLoop中,但是有一些細節的問題比較抓狂,比如流暢度優化,計算frame等,需要對UIScrollView有一定的理解,核心代碼如下。
    func scrollAtEdge(){
        //計算拖動視圖里邊緣的距離,正比于滾動速度,并且判斷是往上還是往下滾動
        let pinTop = dragView.frame.origin.y
        let pinBottom = self.frame.height - (dragView.frame.origin.y + dragView.frame.height)
        var speed:CGFloat = 0
        var isTop:Bool = true
        if pinTop < 0 {
            speed = -pinTop
            isTop = true
        } else if pinBottom < 0 {
            speed = -pinBottom
            isTop = false
        } else {
            self.timer?.invalidate()
            self.timer = nil
            return
        }
        if let originTimer = self.timer,originSpeed = (originTimer.userInfo as? [String:AnyObject])?["speed"] as? CGFloat{
            //計算滾動速度和原來相差是否過大,目的是防止頻繁的創建定時器而使滾動卡頓
            if abs(speed - originSpeed) > 10 {
                originTimer.invalidate()
                NSLog("speed:\(speed)")
                // 60fps,滾動才能流暢
                let timer = NSTimer(timeInterval: 1/60.0, target: self, selector: #selector(SortableCollectionView.autoScroll(_:)), userInfo: ["top":isTop,"speed": speed] , repeats: true)
                self.timer = timer
                NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
            }
        } else {
            let timer = NSTimer(timeInterval: 1/60.0, target: self, selector: #selector(SortableCollectionView.autoScroll(_:)), userInfo: ["top":isTop,"speed": speed] , repeats: true)
            self.timer = timer
            NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
        }
    }
    
    
    func autoScroll(timer:NSTimer) {
        if let userInfo = timer.userInfo as? [String:AnyObject] {
            if let top =  userInfo["top"] as? Bool,speed = userInfo["speed"] as? CGFloat {
                //計算滾動位置,更新contentOffset
                let offset = speed / 5
                let contentOffset = self.contentOffset
                if top {
                    self.contentOffset.y -= offset
                    self.contentOffset.y = self.contentOffset.y < 0 ? 0 : self.contentOffset.y
                }else {
                    self.contentOffset.y += offset
                    self.contentOffset.y = self.contentOffset.y > self.contentSize.height - self.frame.height ? self.contentSize.height - self.frame.height  : self.contentOffset.y
                }
                let point = CGPoint(x: dragView.center.x, y: dragView.center.y + contentOffset.y)
                //滾動過程中,拖拽視圖位置不變,因此手勢識別代理不會調用,需要手動調用移動item
                self.moveItemToPoint(point)
            }
        }
    }

待改進

  • 增加代理方法,對顯示中的Cell進行操作,比如增加抖動動畫等。
  • 極少數情況下滾動會短時間掉幀,可能是RunLoop操作的問題。
    目前正在完善中,各位小伙伴有好的意見建議都可以留言評論,喜歡的小伙伴給個Star唄!~
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 iOS里的UI控件其實沒有幾個,界面基本就是圍繞那么幾個控件靈活展開,最難的應屬UICollectionVi...
    alenpaulkevin閱讀 32,083評論 9 176
  • 翻譯自“Collection View Programming Guide for iOS” 0 關于iOS集合視...
    lakerszhy閱讀 3,926評論 1 22
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,257評論 4 61
  • 我知道你一直在 1 水,一滴一滴,匯聚成大海,人,一個一個,分散至天涯。而經歷了泥土花草,山川小溪,懸崖溝壑最后還...
    困了_b724閱讀 192評論 0 0
  • 01 我猜想,很多人的心里,或許都曾和我一樣想過“以夢為馬,仗劍走天涯”。 但實際,有多少人和我一樣,坐在不足10...
    雨青時間閱讀 608評論 0 0