Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions Example/DeletableListViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// Copyright (c) 2016 Adam Shin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import UIKit
import SwiftReorder

class DeletableViewController: UIViewController {

var items = (1...10).map { "Item \($0)" }
private var tableView: UITableView!
private var deleteButton: UIView?

override func viewDidLoad() {
super.viewDidLoad()

title = "Deletable"

tableView = UITableView(frame: view.frame, style: .grouped)
tableView.dataSource = self
view.addSubview(tableView)
view.backgroundColor = .white

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.allowsSelection = false
tableView.reorder.delegate = self
}

private func showDeleteButton() {
let button = UIView()
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)

let bottomAnchor: NSLayoutYAxisAnchor
if #available(iOS 11, *) {
bottomAnchor = view.safeAreaLayoutGuide.bottomAnchor
} else {
bottomAnchor = view.bottomAnchor
}
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: view.leadingAnchor),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.heightAnchor.constraint(equalToConstant: 50),
button.bottomAnchor.constraint(equalTo: bottomAnchor)
])

let label = UILabel()
label.text = "💣"
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
button.addSubview(label)

NSLayoutConstraint.activate([
label.widthAnchor.constraint(equalToConstant: 50),
label.heightAnchor.constraint(equalToConstant: 50),
label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
])

deleteButton = button

if let snapshot = tableView.reorder.snapshotView {
snapshot.superview?.bringSubviewToFront(snapshot)
}
}

private func removeDeleteButton() {
deleteButton?.removeFromSuperview()
}

private func handleCellMove(with gestureRecognizer: UIGestureRecognizer) {
guard let deletionRatio = getDeletionRatio(from: gestureRecognizer),
let snapshot = tableView.reorder.snapshotView else {
return
}

if deletionRatio > 0 {
snapshot.alpha = 1 - deletionRatio
} else {
snapshot.alpha = 1
}
}

private func getDeletionRatio(from gestureRecognizer: UIGestureRecognizer) -> CGFloat? {
guard let deleteButton = deleteButton else {
return nil
}
let position = gestureRecognizer.location(in: deleteButton).y
let targetPosition = deleteButton.frame.height / 2
let targetSize = deleteButton.frame.height
let deletionRatio = max(1 - abs(position - targetPosition) / targetSize, 0)
return deletionRatio
}
}

extension DeletableViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let spacer = tableView.reorder.spacerCell(for: indexPath) {
return spacer
}

let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]

return cell
}

}

extension DeletableViewController: TableViewReorderDelegate {

func tableView(_ tableView: UITableView, reorderRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = items[sourceIndexPath.row]
items.remove(at: sourceIndexPath.row)
items.insert(item, at: destinationIndexPath.row)
}

func tableViewDidBeginReordering(_ tableView: UITableView, at indexPath: IndexPath) {
showDeleteButton()
}

func tableViewShouldRemoveCell(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath: IndexPath, with gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let deletionRatio = getDeletionRatio(from: gestureRecognizer) else {
return false
}
if deletionRatio > 0.5 {
return true
}
return false
}

func tableViewDidMoveCell(_ tableView: UITableView, with gestureRecognizer: UIGestureRecognizer) {
handleCellMove(with: gestureRecognizer)
}

func tableViewDidFinishReordering(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath: IndexPath) {
removeDeleteButton()
}

func tableViewDidFinishReorderingWithDeletion(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, last lastIndexPath: IndexPath) {
items.remove(at: finalDestinationIndexPath.row)
removeDeleteButton()
}
}
5 changes: 5 additions & 0 deletions Example/RootViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class RootViewController: UITableViewController {
case nonMovable
case effects
case customCells
case deletable

case count
}
Expand Down Expand Up @@ -75,6 +76,8 @@ extension RootViewController {
cell.textLabel?.text = "Effects"
case .customCells:
cell.textLabel?.text = "Custom Cells"
case .deletable:
cell.textLabel?.text = "Deletable Cells"
case .count:
break
}
Expand All @@ -97,6 +100,8 @@ extension RootViewController {
navigationController?.pushViewController(EffectsViewController(), animated: true)
case .customCells:
navigationController?.pushViewController(CustomCellsViewController(), animated: true)
case .deletable:
navigationController?.pushViewController(DeletableViewController(), animated: true)
case .count:
break
}
Expand Down
4 changes: 2 additions & 2 deletions Source/ReorderController+GestureRecognizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ extension ReorderController {
beginReorder(touchPosition: touchPosition)

case .changed:
updateReorder(touchPosition: touchPosition)
updateReorder(touchPosition: touchPosition, gestureRecognizer: gestureRecognizer)

case .ended, .cancelled, .failed, .possible:
endReorder()
endReorder(gestureRecognizer: gestureRecognizer)
@unknown default: break
}
}
Expand Down
74 changes: 67 additions & 7 deletions Source/ReorderController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,36 @@ public protocol TableViewReorderDelegate: class {
func tableViewDidBeginReordering(_ tableView: UITableView, at indexPath: IndexPath)

/**
Tells the delegate that the user has finished reordering.
Tells the delegate that the user has finished reordering and that the row was moved.
- Parameter tableView: The table view providing this information.
- Parameter initialSourceIndexPath: The initial index path of the selected row, before reordering began.
- Parameter finalDestinationIndexPath: The final index path of the selected row.
*/
func tableViewDidFinishReordering(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath: IndexPath)

/**
Tells the delegate that the user has finished reordering and that the row was deleted.
- Parameter tableView: The table view providing this information.
- Parameter initialSourceIndexPath: The initial index path of the selected row, before reordering began.
- Parameter lastIndexPath: The last index path of cell before it got deleted
*/
func tableViewDidFinishReorderingWithDeletion(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, last lastIndexPath: IndexPath)

/**
Tells the delegate that the user has moved the cell
- Parameter tableView: The table view providing this information.
- Parameter gestureRecognizer: The user gesture that moved the cell.
*/
func tableViewDidMoveCell(_ tableView: UITableView, with gestureRecognizer: UIGestureRecognizer)

/**
Asks the delegate whether the cell should be deleted
- Parameter tableView: The table view providing this information.
- Parameter initialSourceIndexPath: The initial index path of the selected row, before reordering began.
- Parameter finalDestinationIndexPath: The current index path of the selected row.
- Parameter gestureRecognizer: The user gesture that moved the cell and has now ended.
*/
func tableViewShouldRemoveCell(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath: IndexPath, with gestureRecognizer: UIGestureRecognizer) -> Bool
}

public extension TableViewReorderDelegate {
Expand All @@ -98,6 +121,15 @@ public extension TableViewReorderDelegate {
func tableViewDidFinishReordering(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath:IndexPath) {
}

func tableViewDidMoveCell(_ tableView: UITableView, with gestureRecognizer: UIGestureRecognizer) {
}

func tableViewShouldRemoveCell(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, to finalDestinationIndexPath: IndexPath, with gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}

func tableViewDidFinishReorderingWithDeletion(_ tableView: UITableView, from initialSourceIndexPath: IndexPath, last lastIndexPath: IndexPath) {
}
}

// MARK: - ReorderController
Expand Down Expand Up @@ -156,6 +188,9 @@ public class ReorderController: NSObject {
/// Whether or not autoscrolling is enabled
public var autoScrollEnabled = true

/// The view used to represent the cell being reordered
public internal(set) var snapshotView: UIView? = nil

/**
Returns a `UITableViewCell` if the table view should display a spacer cell at the given index path.

Expand Down Expand Up @@ -198,7 +233,6 @@ public class ReorderController: NSObject {
weak var tableView: UITableView?

var reorderState: ReorderState = .ready(snapshotRow: nil)
var snapshotView: UIView? = nil

var autoScrollDisplayLink: CADisplayLink?
var lastAutoScrollTimeStamp: CFTimeInterval?
Expand Down Expand Up @@ -255,18 +289,20 @@ public class ReorderController: NSObject {
delegate.tableViewDidBeginReordering(tableView, at: sourceRow)
}

func updateReorder(touchPosition: CGPoint) {
guard case .reordering(let context) = reorderState else { return }
func updateReorder(touchPosition: CGPoint, gestureRecognizer: UIGestureRecognizer) {
guard case .reordering(let context) = reorderState, let tableView = tableView else { return }

var newContext = context
newContext.touchPosition = touchPosition
reorderState = .reordering(context: newContext)

updateSnapshotViewPosition()
updateDestinationRow()

delegate?.tableViewDidMoveCell(tableView, with: gestureRecognizer)
}

func endReorder() {
func endReorder(gestureRecognizer: UIGestureRecognizer) {
guard case .reordering(let context) = reorderState,
let tableView = tableView,
let superview = tableView.superview
Expand All @@ -284,16 +320,40 @@ public class ReorderController: NSObject {
snapshotView?.center.y += 0.1
}

guard delegate?.tableViewShouldRemoveCell(
tableView,
from: context.sourceRow,
to: context.destinationRow,
with: gestureRecognizer
) != true else {
removeSnapshotView()
clearAutoScrollDisplayLink()
reorderState = .ready(snapshotRow: nil)

UIView.performWithoutAnimation {
delegate?.tableViewDidFinishReorderingWithDeletion(tableView, from: context.sourceRow, last: context.destinationRow)
tableView.deleteRows(at: [context.destinationRow], with: .none)
}
return
}

UIView.animate(withDuration: animationDuration,
animations: {
self.snapshotView?.center = CGPoint(x: cellRect.midX, y: cellRect.midY)
},
completion: { _ in
if case let .ready(snapshotRow) = self.reorderState, let row = snapshotRow {
self.reorderState = .ready(snapshotRow: nil)
UIView.performWithoutAnimation {
tableView.reloadRows(at: [row], with: .none)

if let rowCount = tableView.dataSource?.tableView(tableView, numberOfRowsInSection: row.section),
rowCount > row.row {
// else the row has been removed during the animation and calling
// the following would crash
UIView.performWithoutAnimation {
tableView.reloadRows(at: [row], with: .none)
}
}

self.removeSnapshotView()
}
}
Expand Down
4 changes: 4 additions & 0 deletions SwiftReorder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
63B93AE2229E247E00D62F10 /* DeletableListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B93AE1229E247E00D62F10 /* DeletableListViewController.swift */; };
66FC50F11D5EE49D00CFCCCE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FC50E81D5EE49D00CFCCCE /* AppDelegate.swift */; };
66FC50F21D5EE49D00CFCCCE /* BasicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FC50E91D5EE49D00CFCCCE /* BasicViewController.swift */; };
66FC50F41D5EE49D00CFCCCE /* EffectsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FC50EB1D5EE49D00CFCCCE /* EffectsViewController.swift */; };
Expand Down Expand Up @@ -51,6 +52,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
63B93AE1229E247E00D62F10 /* DeletableListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletableListViewController.swift; sourceTree = "<group>"; };
660256931CE6A5170029CB5F /* SwiftReorderExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftReorderExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
66DF25101D8080A000C19289 /* ReorderController+AutoScroll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ReorderController+AutoScroll.swift"; path = "Source/ReorderController+AutoScroll.swift"; sourceTree = "<group>"; };
66DF25111D8080A000C19289 /* ReorderController+DestinationRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ReorderController+DestinationRow.swift"; path = "Source/ReorderController+DestinationRow.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -122,6 +124,7 @@
66FC50EB1D5EE49D00CFCCCE /* EffectsViewController.swift */,
AB6E455B21A82AAD00D47CFC /* CustomCellsViewController.swift */,
66FC50ED1D5EE49D00CFCCCE /* Info.plist */,
63B93AE1229E247E00D62F10 /* DeletableListViewController.swift */,
);
path = Example;
sourceTree = "<group>";
Expand Down Expand Up @@ -262,6 +265,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
63B93AE2229E247E00D62F10 /* DeletableListViewController.swift in Sources */,
66FC50F91D5EE49D00CFCCCE /* RootViewController.swift in Sources */,
AB6E455C21A82AAD00D47CFC /* CustomCellsViewController.swift in Sources */,
66FC50F81D5EE49D00CFCCCE /* NonMovableViewController.swift in Sources */,
Expand Down