├── .gitignore ├── LICENSE ├── LPRTableView.podspec ├── LPRTableView.swift ├── README.md ├── ReorderTest ├── ReorderTest.xcodeproj │ └── project.pbxproj └── ReorderTest │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── DetailViewController.swift │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Info.plist │ └── MasterViewController.swift └── SampleScreenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcworkspace 3 | *.xcuserstate 4 | *xcuserdata* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nicolas Gomollon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LPRTableView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "LPRTableView" 3 | spec.version = "1.0.3" 4 | spec.summary = "A drop-in replacement for UITableView and UITableViewController that supports long-press reordering of cells." 5 | spec.homepage = "https://github.com/nicolasgomollon/LPRTableView" 6 | spec.platform = :ios, "12.0" 7 | spec.swift_versions = ["5.0"] 8 | spec.author = "Nicolas Gomollon" 9 | spec.license = "MIT" 10 | spec.source = { :git => "https://github.com/nicolasgomollon/LPRTableView.git", :tag => "#{spec.version}" } 11 | spec.source_files = "*.swift" 12 | spec.exclude_files = "ReorderTest/**" 13 | end -------------------------------------------------------------------------------- /LPRTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LPRTableView.swift 3 | // LPRTableView 4 | // 5 | // Objective-C code Copyright (c) 2013 Ben Vogelzang. All rights reserved. 6 | // Swift adaptation Copyright (c) 2014 Nicolas Gomollon. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | import UIKit 11 | 12 | /// The delegate of a `LPRTableView` object can adopt the `LPRTableViewDelegate` protocol. 13 | /// Optional methods of the protocol allow the delegate to modify a cell visually before dragging occurs, or to be notified when a cell is about to be dragged or about to be dropped. 14 | @objc 15 | public protocol LPRTableViewDelegate: NSObjectProtocol { 16 | 17 | /// Asks the delegate whether a given row can be moved to another location in the table view based on the gesture location. 18 | /// 19 | /// The default is `true`. 20 | @objc optional func tableView(_ tableView: UITableView, shouldMoveRowAtIndexPath indexPath: IndexPath, forDraggingGesture gesture: UILongPressGestureRecognizer) -> Bool 21 | 22 | /// Provides the delegate a chance to modify the cell visually before dragging occurs. 23 | /// 24 | /// Defaults to using the cell as-is if not implemented. 25 | @objc optional func tableView(_ tableView: UITableView, draggingCell cell: UITableViewCell, at indexPath: IndexPath) -> UITableViewCell 26 | 27 | /// Called within an animation block when the dragging view is about to show. 28 | @objc optional func tableView(_ tableView: UITableView, showDraggingView view: UIView, at indexPath: IndexPath) 29 | 30 | /// Called within an animation block when the dragging view is about to hide. 31 | @objc optional func tableView(_ tableView: UITableView, hideDraggingView view: UIView, at indexPath: IndexPath) 32 | 33 | /// Called when the dragging gesture's vertical location changes. 34 | @objc optional func tableView(_ tableView: UITableView, draggingGestureChanged gesture: UILongPressGestureRecognizer) 35 | 36 | } 37 | 38 | open class LPRTableView: UITableView { 39 | 40 | /// The object that acts as the delegate of the receiving table view. 41 | weak open var longPressReorderDelegate: LPRTableViewDelegate? 42 | 43 | fileprivate var longPressGestureRecognizer: UILongPressGestureRecognizer! 44 | 45 | fileprivate var initialIndexPath: IndexPath? 46 | 47 | fileprivate var currentLocationIndexPath: IndexPath? 48 | 49 | fileprivate var draggingView: UIView? 50 | 51 | fileprivate var scrollRate: Double = 0.0 52 | 53 | fileprivate var scrollDisplayLink: CADisplayLink? 54 | 55 | fileprivate var feedbackGenerator: AnyObject? 56 | 57 | fileprivate var previousGestureVerticalPosition: CGFloat? 58 | 59 | /// A Bool property that indicates whether long press to reorder is enabled. 60 | open var longPressReorderEnabled: Bool { 61 | get { 62 | return longPressGestureRecognizer.isEnabled 63 | } 64 | set { 65 | longPressGestureRecognizer.isEnabled = newValue 66 | } 67 | } 68 | 69 | /// The minimum period a finger must press on a cell for the reordering to begin. 70 | /// 71 | /// The time interval is in seconds. The default duration is `0.5` seconds. 72 | open var minimumPressDuration: TimeInterval { 73 | get { 74 | return longPressGestureRecognizer.minimumPressDuration 75 | } 76 | set { 77 | longPressGestureRecognizer.minimumPressDuration = newValue 78 | } 79 | } 80 | 81 | public convenience init() { 82 | self.init(frame: .zero) 83 | } 84 | 85 | public override init(frame: CGRect, style: UITableView.Style) { 86 | super.init(frame: frame, style: style) 87 | initialize() 88 | } 89 | 90 | public required init?(coder aDecoder: NSCoder) { 91 | super.init(coder: aDecoder) 92 | initialize() 93 | } 94 | 95 | fileprivate func initialize() { 96 | longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(LPRTableView._longPress(_:))) 97 | longPressGestureRecognizer.delegate = self 98 | addGestureRecognizer(longPressGestureRecognizer) 99 | } 100 | 101 | } 102 | 103 | extension LPRTableView: UIGestureRecognizerDelegate { 104 | 105 | open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 106 | guard gestureRecognizer == longPressGestureRecognizer else { return true } 107 | let location: CGPoint = gestureRecognizer.location(in: self) 108 | let indexPath: IndexPath? = indexPathForRow(at: location) 109 | let rows: Int = (0.. 0) 113 | && (indexPath != nil) 114 | && canMoveRowAt(indexPath: indexPath!) 115 | && shouldMoveRowAt(indexPath: indexPath!, forDraggingGesture: longPressGestureRecognizer) 116 | } 117 | 118 | } 119 | 120 | extension LPRTableView { 121 | 122 | fileprivate func canMoveRowAt(indexPath: IndexPath) -> Bool { 123 | return dataSource?.tableView?(self, canMoveRowAt: indexPath) ?? true 124 | } 125 | 126 | fileprivate func shouldMoveRowAt(indexPath: IndexPath, forDraggingGesture gesture: UILongPressGestureRecognizer) -> Bool { 127 | return longPressReorderDelegate?.tableView?(self, shouldMoveRowAtIndexPath: indexPath, forDraggingGesture: longPressGestureRecognizer) ?? true 128 | } 129 | 130 | @objc internal func _longPress(_ gesture: UILongPressGestureRecognizer) { 131 | let location: CGPoint = gesture.location(in: self) 132 | let indexPath: IndexPath? = indexPathForRow(at: location) 133 | 134 | switch gesture.state { 135 | case .began: // Started 136 | hapticFeedbackSetup() 137 | hapticFeedbackSelectionChanged() 138 | previousGestureVerticalPosition = location.y 139 | 140 | guard let indexPath: IndexPath = indexPath, 141 | var cell: UITableViewCell = cellForRow(at: indexPath) else { break } 142 | endEditing(true) 143 | cell.setSelected(false, animated: false) 144 | cell.setHighlighted(false, animated: false) 145 | 146 | // Create the view that will be dragged around the screen. 147 | if draggingView == nil { 148 | if let draggingCell: UITableViewCell = longPressReorderDelegate?.tableView?(self, draggingCell: cell, at: indexPath) { 149 | cell = draggingCell 150 | } 151 | 152 | // Take a snapshot of the pressed table view cell. 153 | draggingView = cell.snapshotView(afterScreenUpdates: false) 154 | 155 | if let draggingView: UIView = draggingView { 156 | addSubview(draggingView) 157 | let rect: CGRect = rectForRow(at: indexPath) 158 | draggingView.frame = draggingView.bounds.offsetBy(dx: rect.origin.x, dy: rect.origin.y) 159 | 160 | UIView.beginAnimations("LongPressReorder-ShowDraggingView", context: nil) 161 | longPressReorderDelegate?.tableView?(self, showDraggingView: draggingView, at: indexPath) 162 | UIView.commitAnimations() 163 | 164 | // Add drop shadow to image and lower opacity. 165 | draggingView.layer.masksToBounds = false 166 | draggingView.layer.shadowColor = UIColor.black.cgColor 167 | draggingView.layer.shadowOffset = .zero 168 | draggingView.layer.shadowRadius = 4.0 169 | draggingView.layer.shadowOpacity = 0.7 170 | draggingView.layer.opacity = 0.85 171 | 172 | // Zoom image towards user. 173 | UIView.beginAnimations("LongPressReorder-Zoom", context: nil) 174 | draggingView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) 175 | draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location)) 176 | UIView.commitAnimations() 177 | } 178 | } 179 | 180 | cell.isHidden = true 181 | currentLocationIndexPath = indexPath 182 | initialIndexPath = indexPath 183 | 184 | // Enable scrolling for cell. 185 | scrollDisplayLink = CADisplayLink(target: self, selector: #selector(LPRTableView._scrollTableWithCell(_:))) 186 | scrollDisplayLink?.add(to: .main, forMode: .default) 187 | case .changed: // Dragging 188 | if let draggingView: UIView = draggingView { 189 | // Update position of the drag view 190 | draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location)) 191 | if let previousGestureVerticalPosition: CGFloat = self.previousGestureVerticalPosition { 192 | if location.y != previousGestureVerticalPosition { 193 | longPressReorderDelegate?.tableView?(self, draggingGestureChanged: gesture) 194 | self.previousGestureVerticalPosition = location.y 195 | } 196 | } else { 197 | longPressReorderDelegate?.tableView?(self, draggingGestureChanged: gesture) 198 | self.previousGestureVerticalPosition = location.y 199 | } 200 | } 201 | 202 | let inset: UIEdgeInsets 203 | if #available(iOS 11.0, *) { 204 | inset = adjustedContentInset 205 | } else { 206 | inset = contentInset 207 | } 208 | 209 | var rect: CGRect = bounds 210 | // Adjust rect for content inset, as we will use it below for calculating scroll zones. 211 | rect.size.height -= inset.top 212 | 213 | // Tell us if we should scroll, and in which direction. 214 | let scrollZoneHeight: CGFloat = rect.size.height / 6.0 215 | let bottomScrollBeginning: CGFloat = contentOffset.y + inset.top + rect.size.height - scrollZoneHeight 216 | let topScrollBeginning: CGFloat = contentOffset.y + inset.top + scrollZoneHeight 217 | 218 | if location.y >= bottomScrollBeginning { 219 | // We're in the bottom zone. 220 | scrollRate = Double(location.y - bottomScrollBeginning) / Double(scrollZoneHeight) 221 | } else if location.y <= topScrollBeginning { 222 | // We're in the top zone. 223 | scrollRate = Double(location.y - topScrollBeginning) / Double(scrollZoneHeight) 224 | } else { 225 | scrollRate = 0.0 226 | } 227 | case .ended where currentLocationIndexPath != nil, // Dropped 228 | .cancelled, 229 | .failed: 230 | // Remove previously cached Gesture location 231 | self.previousGestureVerticalPosition = nil 232 | 233 | // Remove scrolling CADisplayLink. 234 | scrollDisplayLink?.invalidate() 235 | scrollDisplayLink = nil 236 | scrollRate = 0.0 237 | 238 | // 239 | // For use only with Xcode UI Testing: 240 | // Set launch argument `"-LPRTableViewUITestingScreenshots", "1"` to disable dropping a cell, 241 | // to facilitate taking a screenshot with a hovering cell. 242 | // 243 | guard !UserDefaults.standard.bool(forKey: "LPRTableViewUITestingScreenshots") else { break } 244 | 245 | // Animate the drag view to the newly hovered cell. 246 | UIView.animate(withDuration: 0.3, animations: { 247 | guard let draggingView: UIView = self.draggingView, 248 | let currentLocationIndexPath: IndexPath = self.currentLocationIndexPath else { return } 249 | UIView.beginAnimations("LongPressReorder-HideDraggingView", context: nil) 250 | self.longPressReorderDelegate?.tableView?(self, hideDraggingView: draggingView, at: currentLocationIndexPath) 251 | UIView.commitAnimations() 252 | let rect: CGRect = self.rectForRow(at: currentLocationIndexPath) 253 | draggingView.transform = .identity 254 | draggingView.frame = draggingView.bounds.offsetBy(dx: rect.origin.x, dy: rect.origin.y) 255 | }, completion: { (finished: Bool) in 256 | self.draggingView?.removeFromSuperview() 257 | 258 | // Reload the rows that were affected just to be safe. 259 | var visibleRows: [IndexPath] = self.indexPathsForVisibleRows ?? [] 260 | if let indexPath: IndexPath = indexPath, 261 | !visibleRows.contains(indexPath) { 262 | visibleRows.append(indexPath) 263 | } 264 | if let currentLocationIndexPath: IndexPath = self.currentLocationIndexPath, 265 | !visibleRows.contains(currentLocationIndexPath) { 266 | visibleRows.append(currentLocationIndexPath) 267 | } 268 | if !visibleRows.isEmpty { 269 | self.reloadRows(at: visibleRows, with: .none) 270 | } 271 | 272 | self.currentLocationIndexPath = nil 273 | self.draggingView = nil 274 | 275 | self.hapticFeedbackSelectionChanged() 276 | self.hapticFeedbackFinalize() 277 | }) 278 | default: 279 | break 280 | } 281 | } 282 | 283 | fileprivate func updateCurrentLocation(_ gesture: UILongPressGestureRecognizer) { 284 | let location: CGPoint = gesture.location(in: self) 285 | guard var indexPath: IndexPath = indexPathForRow(at: location) else { return } 286 | 287 | if let iIndexPath: IndexPath = initialIndexPath, 288 | let ip: IndexPath = delegate?.tableView?(self, targetIndexPathForMoveFromRowAt: iIndexPath, toProposedIndexPath: indexPath) { 289 | indexPath = ip 290 | } 291 | 292 | guard let clIndexPath: IndexPath = currentLocationIndexPath else { return } 293 | let oldHeight: CGFloat = rectForRow(at: clIndexPath).size.height 294 | let newHeight: CGFloat = rectForRow(at: indexPath).size.height 295 | 296 | switch gesture.state { 297 | case .changed: 298 | if let cell: UITableViewCell = cellForRow(at: clIndexPath) { 299 | cell.setSelected(false, animated: false) 300 | cell.setHighlighted(false, animated: false) 301 | cell.isHidden = true 302 | } 303 | default: 304 | break 305 | } 306 | 307 | guard indexPath != clIndexPath, 308 | gesture.location(in: cellForRow(at: indexPath)).y > (newHeight - oldHeight), 309 | canMoveRowAt(indexPath: indexPath) else { return } 310 | 311 | beginUpdates() 312 | moveRow(at: clIndexPath, to: indexPath) 313 | dataSource?.tableView?(self, moveRowAt: clIndexPath, to: indexPath) 314 | currentLocationIndexPath = indexPath 315 | endUpdates() 316 | 317 | hapticFeedbackSelectionChanged() 318 | } 319 | 320 | @objc internal func _scrollTableWithCell(_ sender: CADisplayLink) { 321 | guard let gesture: UILongPressGestureRecognizer = longPressGestureRecognizer else { return } 322 | 323 | let location: CGPoint = gesture.location(in: self) 324 | guard !(location.y.isNaN || location.x.isNaN) else { return } // Explicitly check for out-of-bound touch. 325 | 326 | let yOffset: Double = Double(contentOffset.y) + scrollRate * 10.0 327 | var newOffset: CGPoint = CGPoint(x: contentOffset.x, y: CGFloat(yOffset)) 328 | 329 | let inset: UIEdgeInsets 330 | if #available(iOS 11.0, *) { 331 | inset = adjustedContentInset 332 | } else { 333 | inset = contentInset 334 | } 335 | 336 | if newOffset.y < -inset.top { 337 | newOffset.y = -inset.top 338 | } else if (contentSize.height + inset.bottom) < frame.size.height { 339 | newOffset = contentOffset 340 | } else if newOffset.y > ((contentSize.height + inset.bottom) - frame.size.height) { 341 | newOffset.y = (contentSize.height + inset.bottom) - frame.size.height 342 | } 343 | 344 | contentOffset = newOffset 345 | 346 | if let draggingView: UIView = draggingView { 347 | draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location)) 348 | } 349 | 350 | updateCurrentLocation(gesture) 351 | } 352 | 353 | fileprivate func newYCenter(for draggingView: UIView, with location: CGPoint) -> CGFloat { 354 | let cellCenter: CGFloat = draggingView.frame.height / 2 355 | let bottomBound: CGFloat = contentSize.height - cellCenter 356 | if location.y < cellCenter { 357 | return cellCenter 358 | } else if location.y > bottomBound { 359 | return bottomBound 360 | } 361 | return location.y 362 | } 363 | 364 | } 365 | 366 | extension LPRTableView { 367 | 368 | fileprivate func hapticFeedbackSetup() { 369 | guard #available(iOS 10.0, *) else { return } 370 | let feedbackGenerator = UISelectionFeedbackGenerator() 371 | feedbackGenerator.prepare() 372 | self.feedbackGenerator = feedbackGenerator 373 | } 374 | 375 | fileprivate func hapticFeedbackSelectionChanged() { 376 | guard #available(iOS 10.0, *), 377 | let feedbackGenerator = self.feedbackGenerator as? UISelectionFeedbackGenerator else { return } 378 | feedbackGenerator.selectionChanged() 379 | feedbackGenerator.prepare() 380 | } 381 | 382 | fileprivate func hapticFeedbackFinalize() { 383 | guard #available(iOS 10.0, *) else { return } 384 | self.feedbackGenerator = nil 385 | } 386 | 387 | } 388 | 389 | open class LPRTableViewController: UITableViewController, LPRTableViewDelegate { 390 | 391 | /// Returns the long press to reorder table view managed by the controller object. 392 | open var lprTableView: LPRTableView { return tableView as! LPRTableView } 393 | 394 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 395 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 396 | initialize() 397 | } 398 | 399 | public override init(style: UITableView.Style) { 400 | super.init(style: style) 401 | initialize() 402 | } 403 | 404 | public required init?(coder aDecoder: NSCoder) { 405 | super.init(coder: aDecoder) 406 | initialize() 407 | } 408 | 409 | fileprivate func initialize() { 410 | tableView = LPRTableView() 411 | tableView.dataSource = self 412 | tableView.delegate = self 413 | registerClasses() 414 | lprTableView.longPressReorderDelegate = self 415 | } 416 | 417 | /// Override this method to register custom UITableViewCell subclass(es). DO NOT call `super` within this method. 418 | open func registerClasses() { 419 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 420 | } 421 | 422 | /// Asks the delegate whether a given row can be moved to another location in the table view based on the gesture location. 423 | /// 424 | /// The default is `true`. The default implementation of this method is empty—no need to call `super`. 425 | open func tableView(_ tableView: UITableView, shouldMoveRowAtIndexPath indexPath: IndexPath, forDraggingGesture gesture: UILongPressGestureRecognizer) -> Bool { 426 | return true 427 | } 428 | 429 | /// Provides the delegate a chance to modify the cell visually before dragging occurs. 430 | /// 431 | /// Defaults to using the cell as-is if not implemented. The default implementation of this method is empty—no need to call `super`. 432 | open func tableView(_ tableView: UITableView, draggingCell cell: UITableViewCell, at indexPath: IndexPath) -> UITableViewCell { 433 | // Empty implementation, just to simplify overriding (and to show up in code completion). 434 | return cell 435 | } 436 | 437 | /// Called within an animation block when the dragging view is about to show. 438 | /// 439 | /// The default implementation of this method is empty—no need to call `super`. 440 | open func tableView(_ tableView: UITableView, showDraggingView view: UIView, at indexPath: IndexPath) { 441 | // Empty implementation, just to simplify overriding (and to show up in code completion). 442 | } 443 | 444 | /// Called within an animation block when the dragging view is about to hide. 445 | /// 446 | /// The default implementation of this method is empty—no need to call `super`. 447 | open func tableView(_ tableView: UITableView, hideDraggingView view: UIView, at indexPath: IndexPath) { 448 | // Empty implementation, just to simplify overriding (and to show up in code completion). 449 | } 450 | 451 | /// Called when the dragging gesture's vertical location changes. 452 | /// 453 | /// The default implementation of this method is empty—no need to call `super`. 454 | open func tableView(_ tableView: UITableView, draggingGestureChanged gesture: UILongPressGestureRecognizer) { 455 | // Empty implementation, just to simplify overriding (and to show up in code completion). 456 | } 457 | 458 | } 459 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LPRTableView 2 | 3 | LPRTableView (LPR is short for “Long Press to Reorder”) is a drop-in replacement for UITableView and UITableViewController that supports reordering by simply long-pressing on a cell. LPRTableView is written completely in Swift (adapted from Objective-C, original code by: [bvogelzang/BVReorderTableView](https://github.com/bvogelzang/BVReorderTableView)). 4 | 5 | Sample Screenshot 6 | 7 | 8 | ## Usage 9 | 10 | Simply replace the `UITableView` of your choice with `LPRTableView`, or replace `UITableViewController` with `LPRTableViewController`. _That’s it!_ 11 | 12 | It’s **important** that you update your data source after the user reorders a cell: 13 | 14 | ```swift 15 | override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 16 | // Modify this code as needed to support more advanced reordering, such as between sections. 17 | objects.insert(objects.remove(at: sourceIndexPath.row), at: destinationIndexPath.row) 18 | } 19 | ``` 20 | 21 | It is possible to select which cells can be reordered by implementing the following _optional_ standard `UITableViewDataSource` method (the absence of this method defaults to all cells being reorderable): 22 | 23 | ```swift 24 | override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 25 | // Change this logic to match your needs. 26 | return (indexPath.section == 0) 27 | } 28 | ``` 29 | 30 | Long-press reordering can be disabled entirely by setting a `Bool` to `lprTableView.longPressReorderEnabled`. 31 | 32 | There are also a few _optional_ delegate methods you may implement after setting `lprTableView.longPressReorderDelegate`: 33 | 34 | ```swift 35 | // Asks the delegate whether a given row can be moved to another location in the table view based on the gesture location. 36 | func tableView(_ tableView: UITableView, shouldMoveRowAtIndexPath indexPath: IndexPath, forDraggingGesture gesture: UILongPressGestureRecognizer) -> Bool 37 | 38 | // Provides a chance to modify the cell (visually) before dragging occurs. 39 | // NOTE: Any changes made here should be reverted in `tableView:cellForRowAtIndexPath:` 40 | // to avoid accidentally reusing the modifications. 41 | func tableView(_ tableView: UITableView, draggingCell cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) -> UITableViewCell { 42 | cell.backgroundColor = .green 43 | return cell 44 | } 45 | 46 | // Called within an animation block when the dragging view is about to show. 47 | func tableView(_ tableView: UITableView, showDraggingView view: UIView, at indexPath: NSIndexPath) 48 | 49 | // Called within an animation block when the dragging view is about to hide. 50 | func tableView(_ tableView: UITableView, hideDraggingView view: UIView, at indexPath: NSIndexPath) 51 | 52 | // Called when the dragging gesture's vertical location changes. 53 | func tableView(_ tableView: UITableView, draggingGestureChanged gesture: UILongPressGestureRecognizer) 54 | ``` 55 | 56 | See the ReorderTest demo project included in this repository for a working example of the project, including the code above. 57 | 58 | If you’re replacing `UITableViewController` with `LPRTableViewController` and are using a custom `UITableViewCell` subclass, then you must override `registerClasses()` and register the appropriate table view cell class(es) within this method. **Do not** call `super` within this method. 59 | 60 | ```swift 61 | override func registerClasses() { 62 | tableView.register(MyCustomTableViewCell.self, forCellReuseIdentifier: "Cell") 63 | } 64 | ``` 65 | 66 | 67 | ## Requirements 68 | 69 | Since LPRTableView is written in Swift 5, it requires Xcode 10.2 or above and works on iOS 12 and above. 70 | 71 | 72 | ## License 73 | 74 | LPRTableView is released under the MIT License. 75 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DA0C225B28A5E311001DB94B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA0C225928A5E311001DB94B /* LaunchScreen.storyboard */; }; 11 | DADF8E5A195139F90083BD36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF8E59195139F90083BD36 /* AppDelegate.swift */; }; 12 | DADF8E5C195139F90083BD36 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF8E5B195139F90083BD36 /* MasterViewController.swift */; }; 13 | DADF8E5E195139F90083BD36 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF8E5D195139F90083BD36 /* DetailViewController.swift */; }; 14 | DADF8E61195139F90083BD36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DADF8E5F195139F90083BD36 /* Main.storyboard */; }; 15 | DADF8E63195139F90083BD36 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DADF8E62195139F90083BD36 /* Images.xcassets */; }; 16 | DADF8E7F195163210083BD36 /* LPRTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF8E7E195163210083BD36 /* LPRTableView.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | DA0C225A28A5E311001DB94B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 21 | DADF8E54195139F90083BD36 /* ReorderTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReorderTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | DADF8E58195139F90083BD36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | DADF8E59195139F90083BD36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | DADF8E5B195139F90083BD36 /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; 25 | DADF8E5D195139F90083BD36 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 26 | DADF8E60195139F90083BD36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | DADF8E62195139F90083BD36 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 28 | DADF8E7E195163210083BD36 /* LPRTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LPRTableView.swift; path = ../LPRTableView.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | DADF8E51195139F90083BD36 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | DADF8E4B195139F90083BD36 = { 43 | isa = PBXGroup; 44 | children = ( 45 | DADF8E7E195163210083BD36 /* LPRTableView.swift */, 46 | DADF8E56195139F90083BD36 /* ReorderTest */, 47 | DADF8E55195139F90083BD36 /* Products */, 48 | ); 49 | sourceTree = ""; 50 | usesTabs = 0; 51 | }; 52 | DADF8E55195139F90083BD36 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | DADF8E54195139F90083BD36 /* ReorderTest.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | DADF8E56195139F90083BD36 /* ReorderTest */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | DADF8E59195139F90083BD36 /* AppDelegate.swift */, 64 | DADF8E5B195139F90083BD36 /* MasterViewController.swift */, 65 | DADF8E5D195139F90083BD36 /* DetailViewController.swift */, 66 | DADF8E5F195139F90083BD36 /* Main.storyboard */, 67 | DA0C225928A5E311001DB94B /* LaunchScreen.storyboard */, 68 | DADF8E62195139F90083BD36 /* Images.xcassets */, 69 | DADF8E57195139F90083BD36 /* Supporting Files */, 70 | ); 71 | path = ReorderTest; 72 | sourceTree = ""; 73 | }; 74 | DADF8E57195139F90083BD36 /* Supporting Files */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | DADF8E58195139F90083BD36 /* Info.plist */, 78 | ); 79 | name = "Supporting Files"; 80 | sourceTree = ""; 81 | }; 82 | /* End PBXGroup section */ 83 | 84 | /* Begin PBXNativeTarget section */ 85 | DADF8E53195139F90083BD36 /* ReorderTest */ = { 86 | isa = PBXNativeTarget; 87 | buildConfigurationList = DADF8E72195139F90083BD36 /* Build configuration list for PBXNativeTarget "ReorderTest" */; 88 | buildPhases = ( 89 | DADF8E50195139F90083BD36 /* Sources */, 90 | DADF8E51195139F90083BD36 /* Frameworks */, 91 | DADF8E52195139F90083BD36 /* Resources */, 92 | ); 93 | buildRules = ( 94 | ); 95 | dependencies = ( 96 | ); 97 | name = ReorderTest; 98 | productName = ReorderTest; 99 | productReference = DADF8E54195139F90083BD36 /* ReorderTest.app */; 100 | productType = "com.apple.product-type.application"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | DADF8E4C195139F90083BD36 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastSwiftMigration = 0700; 109 | LastSwiftUpdateCheck = 0700; 110 | LastUpgradeCheck = 1340; 111 | ORGANIZATIONNAME = "Techno-Magic"; 112 | TargetAttributes = { 113 | DADF8E53195139F90083BD36 = { 114 | CreatedOnToolsVersion = 6.0; 115 | LastSwiftMigration = 1020; 116 | }; 117 | }; 118 | }; 119 | buildConfigurationList = DADF8E4F195139F90083BD36 /* Build configuration list for PBXProject "ReorderTest" */; 120 | compatibilityVersion = "Xcode 3.2"; 121 | developmentRegion = en; 122 | hasScannedForEncodings = 0; 123 | knownRegions = ( 124 | en, 125 | Base, 126 | ); 127 | mainGroup = DADF8E4B195139F90083BD36; 128 | productRefGroup = DADF8E55195139F90083BD36 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | DADF8E53195139F90083BD36 /* ReorderTest */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | DADF8E52195139F90083BD36 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | DA0C225B28A5E311001DB94B /* LaunchScreen.storyboard in Resources */, 143 | DADF8E61195139F90083BD36 /* Main.storyboard in Resources */, 144 | DADF8E63195139F90083BD36 /* Images.xcassets in Resources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXResourcesBuildPhase section */ 149 | 150 | /* Begin PBXSourcesBuildPhase section */ 151 | DADF8E50195139F90083BD36 /* Sources */ = { 152 | isa = PBXSourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | DADF8E5E195139F90083BD36 /* DetailViewController.swift in Sources */, 156 | DADF8E5C195139F90083BD36 /* MasterViewController.swift in Sources */, 157 | DADF8E7F195163210083BD36 /* LPRTableView.swift in Sources */, 158 | DADF8E5A195139F90083BD36 /* AppDelegate.swift in Sources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXSourcesBuildPhase section */ 163 | 164 | /* Begin PBXVariantGroup section */ 165 | DA0C225928A5E311001DB94B /* LaunchScreen.storyboard */ = { 166 | isa = PBXVariantGroup; 167 | children = ( 168 | DA0C225A28A5E311001DB94B /* Base */, 169 | ); 170 | name = LaunchScreen.storyboard; 171 | sourceTree = ""; 172 | }; 173 | DADF8E5F195139F90083BD36 /* Main.storyboard */ = { 174 | isa = PBXVariantGroup; 175 | children = ( 176 | DADF8E60195139F90083BD36 /* Base */, 177 | ); 178 | name = Main.storyboard; 179 | sourceTree = ""; 180 | }; 181 | /* End PBXVariantGroup section */ 182 | 183 | /* Begin XCBuildConfiguration section */ 184 | DADF8E70195139F90083BD36 /* Debug */ = { 185 | isa = XCBuildConfiguration; 186 | buildSettings = { 187 | ALWAYS_SEARCH_USER_PATHS = NO; 188 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 189 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 190 | CLANG_CXX_LIBRARY = "libc++"; 191 | CLANG_ENABLE_MODULES = YES; 192 | CLANG_ENABLE_OBJC_ARC = YES; 193 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 194 | CLANG_WARN_BOOL_CONVERSION = YES; 195 | CLANG_WARN_COMMA = YES; 196 | CLANG_WARN_CONSTANT_CONVERSION = YES; 197 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 198 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 199 | CLANG_WARN_EMPTY_BODY = YES; 200 | CLANG_WARN_ENUM_CONVERSION = YES; 201 | CLANG_WARN_INFINITE_RECURSION = YES; 202 | CLANG_WARN_INT_CONVERSION = YES; 203 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 204 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 205 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 206 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 207 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 208 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 209 | CLANG_WARN_STRICT_PROTOTYPES = YES; 210 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 214 | COPY_PHASE_STRIP = NO; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | ENABLE_TESTABILITY = YES; 217 | GCC_C_LANGUAGE_STANDARD = gnu99; 218 | GCC_DYNAMIC_NO_PIC = NO; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_OPTIMIZATION_LEVEL = 0; 221 | GCC_PREPROCESSOR_DEFINITIONS = ( 222 | "DEBUG=1", 223 | "$(inherited)", 224 | ); 225 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 233 | METAL_ENABLE_DEBUG_INFO = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 237 | }; 238 | name = Debug; 239 | }; 240 | DADF8E71195139F90083BD36 /* Release */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 246 | CLANG_CXX_LIBRARY = "libc++"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_COMMA = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 264 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 265 | CLANG_WARN_STRICT_PROTOTYPES = YES; 266 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 270 | COPY_PHASE_STRIP = YES; 271 | ENABLE_NS_ASSERTIONS = NO; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu99; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 282 | METAL_ENABLE_DEBUG_INFO = NO; 283 | SDKROOT = iphoneos; 284 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 285 | VALIDATE_PRODUCT = YES; 286 | }; 287 | name = Release; 288 | }; 289 | DADF8E73195139F90083BD36 /* Debug */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 | INFOPLIST_FILE = ReorderTest/Info.plist; 294 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 295 | PRODUCT_BUNDLE_IDENTIFIER = "com.nicolasgomollon.${PRODUCT_NAME:rfc1034identifier}"; 296 | PRODUCT_NAME = "$(TARGET_NAME)"; 297 | SWIFT_VERSION = 5.0; 298 | }; 299 | name = Debug; 300 | }; 301 | DADF8E74195139F90083BD36 /* Release */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 305 | INFOPLIST_FILE = ReorderTest/Info.plist; 306 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 307 | PRODUCT_BUNDLE_IDENTIFIER = "com.nicolasgomollon.${PRODUCT_NAME:rfc1034identifier}"; 308 | PRODUCT_NAME = "$(TARGET_NAME)"; 309 | SWIFT_VERSION = 5.0; 310 | }; 311 | name = Release; 312 | }; 313 | /* End XCBuildConfiguration section */ 314 | 315 | /* Begin XCConfigurationList section */ 316 | DADF8E4F195139F90083BD36 /* Build configuration list for PBXProject "ReorderTest" */ = { 317 | isa = XCConfigurationList; 318 | buildConfigurations = ( 319 | DADF8E70195139F90083BD36 /* Debug */, 320 | DADF8E71195139F90083BD36 /* Release */, 321 | ); 322 | defaultConfigurationIsVisible = 0; 323 | defaultConfigurationName = Release; 324 | }; 325 | DADF8E72195139F90083BD36 /* Build configuration list for PBXNativeTarget "ReorderTest" */ = { 326 | isa = XCConfigurationList; 327 | buildConfigurations = ( 328 | DADF8E73195139F90083BD36 /* Debug */, 329 | DADF8E74195139F90083BD36 /* Release */, 330 | ); 331 | defaultConfigurationIsVisible = 0; 332 | defaultConfigurationName = Release; 333 | }; 334 | /* End XCConfigurationList section */ 335 | }; 336 | rootObject = DADF8E4C195139F90083BD36 /* Project object */; 337 | } 338 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ReorderTest 4 | // 5 | // Created by Nicolas Gomollon on 6/17/14. 6 | // Copyright (c) 2014 Techno-Magic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // ReorderTest 4 | // 5 | // Created by Nicolas Gomollon on 6/17/14. 6 | // Copyright (c) 2014 Techno-Magic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DetailViewController: UIViewController { 12 | 13 | @IBOutlet var detailDescriptionLabel: UILabel? 14 | 15 | var detailItem: AnyObject? { 16 | didSet { 17 | // Update the view. 18 | configureView() 19 | } 20 | } 21 | 22 | func configureView() { 23 | // Update the user interface for the detail item. 24 | guard let detail: AnyObject = detailItem, 25 | let label: UILabel = detailDescriptionLabel else { return } 26 | label.text = detail.description 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | super.init(coder: aDecoder) 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | // Do any additional setup after loading the view, typically from a nib. 36 | configureView() 37 | } 38 | 39 | override func didReceiveMemoryWarning() { 40 | super.didReceiveMemoryWarning() 41 | // Dispose of any resources that can be recreated. 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarTintParameters 34 | 35 | UINavigationBar 36 | 37 | Style 38 | UIBarStyleDefault 39 | Translucent 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ReorderTest/ReorderTest/MasterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MasterViewController.swift 3 | // ReorderTest 4 | // 5 | // Created by Nicolas Gomollon on 6/17/14. 6 | // Copyright (c) 2014 Techno-Magic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MasterViewController: LPRTableViewController { 12 | 13 | var objects: Array = .init() 14 | 15 | override func awakeFromNib() { 16 | super.awakeFromNib() 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // Do any additional setup after loading the view, typically from a nib. 23 | navigationItem.leftBarButtonItem = editButtonItem 24 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(MasterViewController.insertNewObject(_:))) 25 | 26 | if #available(iOS 11.0, *) { 27 | navigationController?.navigationBar.prefersLargeTitles = true 28 | } 29 | } 30 | 31 | override func didReceiveMemoryWarning() { 32 | super.didReceiveMemoryWarning() 33 | // Dispose of any resources that can be recreated. 34 | } 35 | 36 | @objc func insertNewObject(_ sender: AnyObject) { 37 | objects.insert(Date(), at: 0) 38 | let indexPath: IndexPath = IndexPath(row: 0, section: 0) 39 | tableView.insertRows(at: [indexPath], with: .automatic) 40 | } 41 | 42 | // MARK: - Table View 43 | 44 | override func numberOfSections(in tableView: UITableView) -> Int { 45 | return 1 46 | } 47 | 48 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 49 | return objects.count 50 | } 51 | 52 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 53 | let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 54 | 55 | let object: Date = objects[indexPath.row] 56 | cell.textLabel?.text = object.description 57 | 58 | // 59 | // Reset any possible modifications made in `tableView:draggingCell:atIndexPath:` 60 | // to avoid reusing the modified cell. 61 | // 62 | // cell.backgroundColor = .white 63 | 64 | return cell 65 | } 66 | 67 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 68 | // Return false if you do not want the specified item to be editable. 69 | return true 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 73 | if editingStyle == .delete { 74 | objects.remove(at: indexPath.row) 75 | tableView.deleteRows(at: [indexPath], with: .fade) 76 | } else if editingStyle == .insert { 77 | // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. 78 | } 79 | } 80 | 81 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 82 | let object: Date = objects[indexPath.row] 83 | let detailViewController: DetailViewController = storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController 84 | detailViewController.detailItem = object as AnyObject? 85 | navigationController?.pushViewController(detailViewController, animated: true) 86 | } 87 | 88 | // MARK: - Segues 89 | 90 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 91 | guard segue.identifier == "showDetail", 92 | let indexPath: IndexPath = tableView.indexPathForSelectedRow else { return } 93 | let object: Date = objects[indexPath.row] 94 | (segue.destination as! DetailViewController).detailItem = object as AnyObject? 95 | } 96 | 97 | // MARK: - Long Press Reorder 98 | 99 | // 100 | // Important: Update your data source after the user reorders a cell. 101 | // 102 | override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 103 | objects.insert(objects.remove(at: sourceIndexPath.row), at: destinationIndexPath.row) 104 | } 105 | 106 | // 107 | // Optional: Modify the cell (visually) before dragging occurs. 108 | // 109 | // NOTE: Any changes made here should be reverted in `tableView:cellForRowAtIndexPath:` 110 | // to avoid accidentally reusing the modifications. 111 | // 112 | override func tableView(_ tableView: UITableView, draggingCell cell: UITableViewCell, at indexPath: IndexPath) -> UITableViewCell { 113 | // cell.backgroundColor = UIColor(red: 165.0/255.0, green: 228.0/255.0, blue: 255.0/255.0, alpha: 1.0) 114 | return cell 115 | } 116 | 117 | // 118 | // Optional: Called within an animation block when the dragging view is about to show. 119 | // 120 | override func tableView(_ tableView: UITableView, showDraggingView view: UIView, at indexPath: IndexPath) { 121 | print("The dragged cell is about to be animated!") 122 | } 123 | 124 | // 125 | // Optional: Called within an animation block when the dragging view is about to hide. 126 | // 127 | override func tableView(_ tableView: UITableView, hideDraggingView view: UIView, at indexPath: IndexPath) { 128 | print("The dragged cell is about to be dropped.") 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /SampleScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgomollon/LPRTableView/0374234383f2ead7304b798a43c1e28a2ac29794/SampleScreenshot.png --------------------------------------------------------------------------------