Swift開發:仿Clear手勢操做(拖拽、划動、捏合)UITableView

這是一個徹底依靠手勢的操做ToDoList的演示,功能上左劃刪除,右劃完成任務,拖拽調整順序,捏合張開插入。git

項目源碼: https://github.com/luan-ma/ClearStyleDemo.Swift github

初始化

TDCToDoItem.swift   定義模型對象web

TDCToDoListController.swift 繼承自UITableViewController, 演示UITableView操做
swift

var items = [
    TDCToDoItem(text: "Feed the cat"),
    TDCToDoItem(text: "Buy eggs"),
    TDCToDoItem(text: "Pack bags for WWDC"),
    TDCToDoItem(text: "Rule the web"),
    TDCToDoItem(text: "Buy a new iPhone"),
    TDCToDoItem(text: "Find missing socks"),
    TDCToDoItem(text: "Write a new tutorial"),
    TDCToDoItem(text: "Master Objective-C"),
    TDCToDoItem(text: "Remember your wedding anniversary!"),
    TDCToDoItem(text: "Drink less beer"),
    TDCToDoItem(text: "Learn to draw"),
    TDCToDoItem(text: "Take the car to the garage"),
    TDCToDoItem(text: "Sell things on eBay"),
    TDCToDoItem(text: "Learn to juggle"),
    TDCToDoItem(text: "Give up")
]

override func viewDidLoad() {
    super.viewDidLoad()

    //捏合手勢
    let pinch = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
    //長按拖拽
    let longPress = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")

    tableView.addGestureRecognizer(pinch)
    tableView.addGestureRecognizer(longPress)
}


左劃刪除、右劃完成

在每個Cell添加滑動手勢(Pan)。處理划動距離,超過寬度1/3就爲有效操做,左劃爲刪除操做,右劃爲完成操做。
less

佈局使用AutoLayout,中間內容區的限制條件是寬度等於容器寬度、高度等於容器高度、垂直中對齊、水平中對齊,而平移操做實際上就是操做水平中對齊的距離值。ide

TDCToDoItemCell.swift關鍵代碼以下工具

手勢判斷佈局

// 若是是划動手勢,僅支持左右划動;若是是其它手勢,則有父類負責
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
    if let panGesture = gestureRecognizer as? UIPanGestureRecognizer {
        let translation = panGesture.translationInView(self.superview)
        return fabs(translation.x) > fabs(translation.y)
    } else {
        return super.gestureRecognizerShouldBegin(gestureRecognizer)
    }
}

手勢操做lua

var onDelete: ((TDCToDoItemCell) -> Void)?
var onComplete: ((TDCToDoItemCell) -> Void)?

private var deleteOnDragRelease: Bool = false
private var completeOnDragRelease: Bool = false

// 划動平移的實際AutoLayout中的水平中對齊的距離
@IBOutlet weak var centerConstraint: NSLayoutConstraint!
private var originConstant: CGFloat = 0

func handlePan(panGesture: UIPanGestureRecognizer) {
    switch panGesture.state {
    case .Began:
        originConstant = centerConstraint.constant
    case .Changed:
        let translation = panGesture.translationInView(self)
        centerConstraint.constant = translation.x

        // 划動移動1/3寬度爲有效划動
        let finished = fabs(translation.x) > CGRectGetWidth(bounds) / 3
        if translation.x < originConstant { // 右劃
            if finished {
                deleteOnDragRelease = true
                rightLabel.textColor = UIColor.redColor()
            } else {
                deleteOnDragRelease = false
                rightLabel.textColor = UIColor.whiteColor()
            }
        } else { // 左劃
            if finished {
                completeOnDragRelease = true
                leftLabel.textColor = UIColor.greenColor()
            } else {
                completeOnDragRelease = false
                leftLabel.textColor = UIColor.whiteColor()
            }
        }
    case .Ended:
        centerConstraint.constant = originConstant

        if deleteOnDragRelease {
            deleteOnDragRelease = false
            if let onDelete = onDelete {
                onDelete(self)
            }
        }

        if completeOnDragRelease {
            completeOnDragRelease = false
            if let onComplete = onComplete {
                onComplete(self)
            }
        }
    default:
        break
    }
}

TDCToDoListController.swift中執行刪除操做spa

/*
// 簡單刪除
func deleteToDoItem(indexPath: NSIndexPath) {
    tableView.beginUpdates()
    items.removeAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    tableView.endUpdates()
}
*/

// 視覺效果更漂亮的刪除
func deleteToDoItem(indexPath: NSIndexPath) {
    let item = items.removeAtIndex(indexPath.row)
    var animationEnabled = false
    let lastCell = tableView.visibleCells.last
    var delay: NSTimeInterval = 0
    for cell in tableView.visibleCells {
        let cell = cell as! TDCToDoItemCell
        if animationEnabled {
            UIView.animateWithDuration(0.25, delay: delay, options: .CurveEaseInOut,
                animations: { () -> Void in
                    cell.frame = CGRectOffset(cell.frame, 0, -CGRectGetHeight(cell.frame))
                }, completion: { (completed) -> Void in
                    if cell == lastCell {
                        self.tableView.reloadData()
                    }
            })
            delay += 0.03
        }

        if cell.toDoItem == item {
            animationEnabled = true
            cell.hidden = true
        }
    }
}


拖拽排序

長按選中某Cell,截圖此Cell生成UIImageView,而後隱藏此Cell(hidden=true),隨手指移動拖拽UIImageView,每次拖拽到一個Cell上的時候,交換當前Cell和隱藏Cell位置。效果以下

截圖UIView生成一個新的UIImageVIew

func snapView(view: UIView) -> UIImageView {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0)
    view.layer.renderInContext(UIGraphicsGetCurrentContext()!)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    let snapShot = UIImageView(image: image)
    snapShot.layer.masksToBounds = false;
    snapShot.layer.cornerRadius = 0;
    snapShot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
    snapShot.layer.shadowOpacity = 0.4;
    snapShot.layer.shadowRadius = 5;
    snapShot.frame = view.frame
    return snapShot
}

拖拽操做代碼,詳細操做參考註釋

private var sourceIndexPath: NSIndexPath?
private var snapView: UIView?

func handleLongPress(longPress: UILongPressGestureRecognizer) {
    let point = longPress.locationInView(tableView)

    if let indexPath = tableView.indexPathForRowAtPoint(point) {
        switch longPress.state {
        case .Began:
            if let cell = tableView.cellForRowAtIndexPath(indexPath) {
                sourceIndexPath = indexPath
                let snapView = self.snapView(cell)
                snapView.alpha = 0

                self.snapView = snapView

                tableView.addSubview(snapView)
                
                UIView.animateWithDuration(0.25, animations: {
                    // 選中Cell跳出放大效果
                    snapView.alpha = 0.95
                    snapView.center = CGPointMake(cell.center.x, point.y)
                    snapView.transform = CGAffineTransformMakeScale(1.05, 1.05)

                    cell.alpha = 0
                    }, completion: { (completed) -> Void in
                        cell.hidden = true
                        cell.alpha = 1
                })
            } else {
                sourceIndexPath = nil
                snapView = nil
                break
            }
        case .Changed:
            if let snapView = snapView {
                // 截圖隨手指上下移動
                snapView.center = CGPointMake(snapView.center.x, point.y)
            }

            // 若是手指移動到一個新的Cell上面,隱藏Cell跟此Cell交換位置
            if let fromIndexPath = sourceIndexPath {
                if fromIndexPath != indexPath {
                    tableView.beginUpdates()
                    let temp = items[indexPath.row]
                    items[indexPath.row] = items[fromIndexPath.row]
                    items[fromIndexPath.row] = temp
                    tableView.moveRowAtIndexPath(fromIndexPath, toIndexPath: indexPath)
                    tableView.endUpdates()
                    sourceIndexPath = indexPath
                }
            }

            // 手指移動到屏幕頂端或底部,UITableView自動滾動
            let step: CGFloat = 64
            if let parentView = tableView.superview {
                let parentPos = tableView.convertPoint(point, toView: parentView)
                if parentPos.y > parentView.bounds.height - step {
                    var offset = tableView.contentOffset
                    offset.y += (parentPos.y - parentView.bounds.height + step)
                    if offset.y > tableView.contentSize.height - tableView.bounds.height {
                        offset.y = tableView.contentSize.height - tableView.bounds.height
                    }
                    tableView.setContentOffset(offset, animated: false)
                } else if parentPos.y <= step {
                    var offset = tableView.contentOffset
                    offset.y -= (step - parentPos.y)
                    if offset.y < 0 {
                        offset.y = 0
                    }
                    tableView.setContentOffset(offset, animated: false)
                }
            }
        default:
            if let snapView = snapView, let fromIndexPath = sourceIndexPath, let cell = tableView.cellForRowAtIndexPath(fromIndexPath) {
                cell.alpha = 0
                cell.hidden = false

                // 長按移動結束,隱藏的Cell恢復顯示,刪除截圖
                UIView.animateWithDuration(0.25, animations: { () -> Void in
                    snapView.center = cell.center
                    snapView.alpha = 0
                    
                    cell.alpha = 1
                    }, completion: { [unowned self] (completed) -> Void in
                        snapView.removeFromSuperview()
                        self.snapView = nil
                        self.sourceIndexPath = nil

                        self.tableView.performSelector("reloadData", withObject: nil, afterDelay: 0.5)
                })
            }
        }
    }
}


捏合張開插入

經過捏合手勢中兩個觸點獲取兩個相鄰的Cell,經過修改UIView.transform屬性移動屏幕上的Cell位置。

獲取Pinch兩個手指座標的工具方法

func pointsOfPinch(pinch: UIPinchGestureRecognizer) -> (CGPoint, CGPoint) {
    if pinch.numberOfTouches() > 1 {
        let point1 = pinch.locationOfTouch(0, inView: tableView)
        let point2 = pinch.locationOfTouch(1, inView: tableView)
        if point1.y <= point2.y {
            return (point1, point2)
        } else {
            return (point2, point1)
        }
    } else {
        let point = pinch.locationOfTouch(0, inView: tableView)
        return (point, point)
    }
}

捏合張開操做代碼,詳情請參考代碼和註釋

// 插入點
private var pinchIndexPath: NSIndexPath?
// 臨時代理視圖
private var placheHolderCell: TDCPlaceHolderView?
// 兩觸點的起始位置
private var sourcePoints: (upperPoint: CGPoint, downPoint: CGPoint)?
// 能夠插入操做的標誌
private var pinchInsertEnabled = false

func handlePinch(pinch: UIPinchGestureRecognizer) {
    switch pinch.state {
    case .Began:
        pinchBegan(pinch)
    case .Changed:
        pinchChanged(pinch)
    default:
        pinchEnd(pinch)
    }
}

func pinchBegan(pinch: UIPinchGestureRecognizer) {
    pinchIndexPath = nil
    sourcePoints = nil
    pinchInsertEnabled = false

    let (upperPoint, downPoint) = pointsOfPinch(pinch)
    if let upperIndexPath = tableView.indexPathForRowAtPoint(upperPoint),
        let downIndexPath = tableView.indexPathForRowAtPoint(downPoint) {
            if downIndexPath.row - upperIndexPath.row == 1 {
                let upperCell = tableView.cellForRowAtIndexPath(upperIndexPath)!
                let placheHolder = NSBundle.mainBundle().loadNibNamed("TDCPlaceHolderView", owner: tableView, options: nil).first as! TDCPlaceHolderView
                placheHolder.frame = CGRectOffset(upperCell.frame, 0, CGRectGetHeight(upperCell.frame) / 2)
                tableView.insertSubview(placheHolder, atIndex: 0)
                
                sourcePoints = (upperPoint, downPoint)
                pinchIndexPath = upperIndexPath
                placheHolderCell = placheHolder
            }
    }
}

func pinchChanged(pinch: UIPinchGestureRecognizer) {
    if let pinchIndexPath = pinchIndexPath, let originPoints = sourcePoints, let placheHolderCell = placheHolderCell {
        let points = pointsOfPinch(pinch)

        let upperDistance = points.0.y - originPoints.upperPoint.y
        let downDistance = originPoints.downPoint.y - points.1.y
        let distance = -min(0, min(upperDistance, downDistance))
        NSLog("distance=\(distance)")
        
        // 移動兩邊的Cell
        for cell in tableView.visibleCells {
            let indexPath = tableView.indexPathForCell(cell)!
            if indexPath.row <= pinchIndexPath.row {
                cell.transform = CGAffineTransformMakeTranslation(0, -distance)
            } else {
                cell.transform = CGAffineTransformMakeTranslation(0, distance)
            }
        }
        
        // 插入的Cell變形
        let scaleY = min(64, fabs(distance) * 2) / CGFloat(64)
        placheHolderCell.transform = CGAffineTransformMakeScale(1, scaleY)
        
        placheHolderCell.lblTitle.text = scaleY <= 0.5 ? "張開雙指插入新項目": "鬆手能夠插入新項目"
        
        // 張開超過一個Cell高度時,執行插入操做
        pinchInsertEnabled = scaleY >= 1
    }
}

func pinchEnd(pinch: UIPinchGestureRecognizer) {
    if let pinchIndexPath = pinchIndexPath, let placheHolderCell = placheHolderCell {
        placheHolderCell.transform = CGAffineTransformIdentity
        placheHolderCell.removeFromSuperview()
        self.placheHolderCell = nil
        
        if pinchInsertEnabled {
            // 恢復各Cell的transform
            for cell in self.tableView.visibleCells {
                cell.transform = CGAffineTransformIdentity
            }

            // 插入操做
            let index = pinchIndexPath.row + 1
            items.insert(TDCToDoItem(text: ""), atIndex: index)
            tableView.reloadData()

            // 彈出鍵盤
            let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as! TDCToDoItemCell
            cell.txtField.becomeFirstResponder()
        } else {
            // 放棄插入,恢復原位置
            UIView.animateWithDuration(0.25, delay: 0, options: .CurveEaseInOut, animations: { [unowned self] () -> Void in
                for cell in self.tableView.visibleCells {
                    cell.transform = CGAffineTransformIdentity
                }
                }, completion: { [unowned self] (completed) -> Void in
                    self.tableView.reloadData()
            })
        }
    }

    sourcePoints = nil
    pinchIndexPath = nil
    pinchInsertEnabled = false
}


參考

1. https://github.com/ColinEberhardt/iOS-ClearStyle

2. http://blog.csdn.net/u013604612/article/details/43884039