Files

372 lines
14 KiB
Swift

//
// TableViewDragger.swift
// TableViewDragger
//
// Created by Kyohei Ito on 2015/09/24.
// Copyright © 2015 kyohei_ito. All rights reserved.
//
import UIKit
@objc public protocol TableViewDraggerDelegate: class {
/// If allow movement of cell, please return `true`. require a call to `moveRowAt:toIndexPath:` of UITableView and rearranged of data.
func dragger(_ dragger: TableViewDragger, moveDraggingAt indexPath: IndexPath, newIndexPath: IndexPath) -> Bool
/// If allow dragging of cell, prease return `true`.
@objc optional func dragger(_ dragger: TableViewDragger, shouldDragAt indexPath: IndexPath) -> Bool
@objc optional func dragger(_ dragger: TableViewDragger, willBeginDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, didBeginDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, willEndDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, didEndDraggingAt indexPath: IndexPath)
}
@objc public protocol TableViewDraggerDataSource: class {
/// Return any cell if want to change the cell in drag.
@objc optional func dragger(_ dragger: TableViewDragger, cellForRowAt indexPath: IndexPath) -> UIView?
/// Return the indexPath if want to change the indexPath to start drag.
@objc optional func dragger(_ dragger: TableViewDragger, indexPathForDragAt indexPath: IndexPath) -> IndexPath
}
open class TableViewDragger: NSObject {
let longPressGesture = UILongPressGestureRecognizer()
let panGesture = UIPanGestureRecognizer()
var draggingCell: TableViewDraggerCell?
var displayLink: CADisplayLink?
var targetClipsToBounds = true
weak var targetTableView: UITableView?
private var draggingDirection: UIScrollView.DraggingDirection?
/// It will be `true` if want to hide the original cell.
open var isHiddenOriginCell: Bool = true
/// Zoom scale of cell in drag.
open var zoomScaleForCell: CGFloat = 1
/// Alpha of cell in drag.
open var alphaForCell: CGFloat = 1
/// Opacity of cell shadow in drag.
open var opacityForShadowOfCell: Float = 0.4
/// Velocity of auto scroll in drag.
open var scrollVelocity: CGFloat = 1
open weak var delegate: TableViewDraggerDelegate?
open weak var dataSource: TableViewDraggerDataSource?
//
open var availableHorizontalScroll : Bool = true
open var tableView: UITableView? {
return targetTableView
}
/// `UITableView` want to drag.
public init(tableView: UITableView, _ minimumPressDuration: CFTimeInterval = 0.5) {
super.init()
self.targetTableView = tableView
tableView.addGestureRecognizer(longPressGesture)
tableView.addGestureRecognizer(panGesture)
longPressGesture.addTarget(self, action: #selector(TableViewDragger.longPressGestureAction(_:)))
longPressGesture.delegate = self
longPressGesture.allowableMovement = 5.0
longPressGesture.minimumPressDuration = minimumPressDuration
panGesture.addTarget(self, action: #selector(TableViewDragger.panGestureAction(_:)))
panGesture.delegate = self
panGesture.maximumNumberOfTouches = 1
}
deinit {
targetTableView?.removeGestureRecognizer(longPressGesture)
targetTableView?.removeGestureRecognizer(panGesture)
}
func targetIndexPath(_ tableView: UITableView, draggingCell: TableViewDraggerCell) -> IndexPath {
let location = draggingCell.location
let offsetY = (draggingCell.viewHeight / 2) + 2
let offsetX = tableView.center.x
let topPoint = CGPoint(x: offsetX, y: location.y - offsetY)
let bottomPoint = CGPoint(x: offsetX, y: location.y + offsetY)
let point = draggingDirection == .up ? topPoint : bottomPoint
if let targetIndexPath = tableView.indexPathForRow(at: point) {
if tableView.cellForRow(at: targetIndexPath) == nil {
return draggingCell.dropIndexPath
}
let targetRect = tableView.rectForRow(at: targetIndexPath)
let targetCenterY = targetRect.origin.y + (targetRect.height / 2)
guard let direction = draggingDirection else {
return draggingCell.dropIndexPath
}
switch direction {
case .up:
if (targetCenterY > point.y && draggingCell.dropIndexPath > targetIndexPath) {
return targetIndexPath
}
case .down:
if (targetCenterY < point.y && draggingCell.dropIndexPath < targetIndexPath) {
return targetIndexPath
}
}
} else {
let section = (0..<tableView.numberOfSections).filter { section -> Bool in
tableView.rect(forSection: section).contains(point)
}.first
if let section = section, tableView.numberOfRows(inSection: section) == 0 {
return IndexPath(row: 0, section: section)
}
}
return draggingCell.dropIndexPath
}
func dragCell(_ tableView: UITableView, draggingCell: TableViewDraggerCell) {
let indexPath = targetIndexPath(tableView, draggingCell: draggingCell)
if draggingCell.dropIndexPath.compare(indexPath) == .orderedSame {
return
}
if let cell = tableView.cellForRow(at: draggingCell.dropIndexPath) {
cell.isHidden = isHiddenOriginCell
}
if delegate?.dragger(self, moveDraggingAt: draggingCell.dropIndexPath, newIndexPath: indexPath) == true {
draggingCell.dropIndexPath = indexPath
}
}
func copiedCell(at indexPath: IndexPath) -> UIView? {
if let view = dataSource?.dragger?(self, cellForRowAt: indexPath) {
return view
}
if let cell = targetTableView?.cellForRow(at: indexPath) {
if let view = cell.snapshotView(afterScreenUpdates: false) {
return view
} else if let view = cell.captured() {
view.frame = cell.bounds
return view
}
}
return nil
}
func draggedCell(_ tableView: UITableView, indexPath: IndexPath) -> TableViewDraggerCell? {
guard let copiedCell = copiedCell(at: indexPath) else {
return nil
}
let cellRect = tableView.rectForRow(at: indexPath)
copiedCell.frame.size = cellRect.size
if let height = tableView.delegate?.tableView?(tableView, heightForRowAt: indexPath) {
copiedCell.frame.size.height = height
}
let cell = TableViewDraggerCell(cell: copiedCell)
cell.dragScale = zoomScaleForCell
cell.dragAlpha = alphaForCell
cell.dragShadowOpacity = opacityForShadowOfCell
cell.dropIndexPath = indexPath
return cell
}
}
// MARK: - Dragging Cell
extension TableViewDragger {
private func draggingDidBegin(_ gesture: UIGestureRecognizer, indexPath: IndexPath) {
displayLink?.invalidate()
displayLink = UIScreen.main.displayLink(withTarget: self, selector: #selector(TableViewDragger.displayDidRefresh(_:)))
displayLink?.add(to: .main, forMode: .default)
displayLink?.isPaused = true
let dragIndexPath = dataSource?.dragger?(self, indexPathForDragAt: indexPath) ?? indexPath
delegate?.dragger?(self, willBeginDraggingAt: dragIndexPath)
if let tableView = targetTableView {
let actualCell = tableView.cellForRow(at: dragIndexPath)
if let draggedCell = draggedCell(tableView, indexPath: dragIndexPath) {
let point = gesture.location(in: actualCell)
if availableHorizontalScroll == true{
draggedCell.offset = point
draggedCell.transformToPoint(point)
draggedCell.location = gesture.location(in: tableView)
} else {
draggedCell.offset = CGPoint(x: (draggedCell.frame.size.width)/2, y: point.y)
draggedCell.transformToPoint(CGPoint(x: (draggedCell.frame.size.width)/2, y: point.y))
draggedCell.location = CGPoint(x: (draggedCell.frame.size.width)/2, y: gesture.location(in: tableView).y)
}
tableView.addSubview(draggedCell)
draggingCell = draggedCell
}
actualCell?.isHidden = isHiddenOriginCell
targetClipsToBounds = tableView.clipsToBounds
tableView.clipsToBounds = false
}
delegate?.dragger?(self, didBeginDraggingAt: indexPath)
}
private func draggingDidChange(_ gesture: UIGestureRecognizer, direction: UIScrollView.DraggingDirection?) {
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
if availableHorizontalScroll == true{
draggingCell.location = gesture.location(in: tableView)
} else {
draggingCell.location = CGPoint(x: (draggingCell.frame.size.width)/2, y: gesture.location(in: tableView).y)
}
if let adjustedDirection = tableView.draggingDirection(at: draggingCell.adjustedCenter(on: tableView)) {
displayLink?.isPaused = false
draggingDirection = adjustedDirection
} else {
draggingDirection = direction
}
dragCell(tableView, draggingCell: draggingCell)
}
private func draggingDidEnd(_ gesture: UIGestureRecognizer) {
displayLink?.invalidate()
displayLink = nil
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
delegate?.dragger?(self, willEndDraggingAt: draggingCell.dropIndexPath)
let targetRect = tableView.rectForRow(at: draggingCell.dropIndexPath)
let center = CGPoint(x: targetRect.width / 2, y: targetRect.origin.y + (targetRect.height / 2))
draggingCell.drop(center) {
self.delegate?.dragger?(self, didEndDraggingAt: draggingCell.dropIndexPath)
if let cell = tableView.cellForRow(at: draggingCell.dropIndexPath) {
cell.isHidden = false
}
tableView.clipsToBounds = self.targetClipsToBounds
self.draggingCell = nil
}
}
}
// MARK: - Action Methods
private extension TableViewDragger {
@objc func displayDidRefresh(_ displayLink: CADisplayLink) {
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
let center = draggingCell.adjustedCenter(on: tableView)
if let direction = tableView.draggingDirection(at: center) {
draggingDirection = direction
} else {
displayLink.isPaused = true
}
tableView.contentOffset = tableView.preferredContentOffset(at: center, velocity: scrollVelocity)
dragCell(tableView, draggingCell: draggingCell)
if availableHorizontalScroll == true{
draggingCell.location = panGesture.location(in: tableView)
} else {
draggingCell.location = CGPoint(x: draggingCell.frame.size.width/2, y: panGesture.location(in: tableView).y)
}
}
@objc func longPressGestureAction(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
targetTableView?.isScrollEnabled = false
let point = gesture.location(in: targetTableView)
if let path = targetTableView?.indexPathForRow(at: point) {
draggingDidBegin(gesture, indexPath: path)
}
case .ended, .cancelled:
draggingDidEnd(gesture)
targetTableView?.isScrollEnabled = true
case .changed, .failed, .possible:
break
@unknown default:
return
}
}
@objc func panGestureAction(_ gesture: UIPanGestureRecognizer) {
guard targetTableView?.isScrollEnabled == false && gesture.state == .changed else {
return
}
let offsetY = gesture.translation(in: targetTableView).y
if offsetY < 0 {
draggingDidChange(gesture, direction: .up)
} else if offsetY > 0 {
draggingDidChange(gesture, direction: .down)
} else {
draggingDidChange(gesture, direction: nil)
}
gesture.setTranslation(.zero, in: targetTableView)
}
}
// MARK: - UIGestureRecognizerDelegate Methods
extension TableViewDragger: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if gestureRecognizer == longPressGesture {
let point = touch.location(in: targetTableView)
if let indexPath = targetTableView?.indexPathForRow(at: point) {
if let ret = delegate?.dragger?(self, shouldDragAt: indexPath) {
return ret
}
} else {
return false
}
}
return true
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == panGesture || otherGestureRecognizer == panGesture || gestureRecognizer == longPressGesture || otherGestureRecognizer == longPressGesture
}
}
extension UIView {
fileprivate func captured() -> UIView? {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) else {
return nil
}
return try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [UIView.self], from: data) as? UIView
}
}