Open RushanB opened 6 years ago
hmm… I’m not sure I quite understand the effect you’re trying to achieve. Would you mind sharing a concrete example (either in code or visually)?
Thanks for the reply! Yes you can view my effect here: https://imgur.com/a/76VRx as well as the changes I made to LPRTableView in order to make it select the header for each section and move the sections by dragging the header. My problem is I cannot get it to scroll up or down when I am dragging the header in each direction as you can do with rows. In my case you would have to scroll the sections down instead of the rows I suppose is where I am struggling.
//
// LPRTableView.swift
// LPRTableView
//
// Objective-C code Copyright (c) 2013 Ben Vogelzang. All rights reserved.
// Swift adaptation Copyright (c) 2014 Nicolas Gomollon. All rights reserved.
//
import Foundation
import QuartzCore
import UIKit
/** The delegate of a LPRTableView object can adopt the LPRTableViewDelegate protocol. 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. */
@objc
public protocol LPRTableViewDelegate: NSObjectProtocol {
/** Provides the delegate a chance to modify the cell visually before dragging occurs. Defaults to using the cell as-is if not implemented. */
@objc optional func tableView(_ tableView: UITableView, draggingCell cell: UITableViewHeaderFooterView, at indexPath: IndexPath) -> UITableViewHeaderFooterView
/** Called within an animation block when the dragging view is about to show. */
@objc optional func tableView(_ tableView: UITableView, showDraggingView view: UIView, at indexPath: IndexPath)
/** Called within an animation block when the dragging view is about to hide. */
@objc optional func tableView(_ tableView: UITableView, hideDraggingView view: UIView, at indexPath: IndexPath)
/** Called when the dragging gesture's vertical location changes. */
@objc optional func tableView(_ tableView: UITableView, draggingGestureChanged gesture: UILongPressGestureRecognizer)
}
open class LPRTableView: UITableView {
/** The object that acts as the delegate of the receiving table view. */
weak open var longPressReorderDelegate: LPRTableViewDelegate?
fileprivate var longPressGestureRecognizer: UILongPressGestureRecognizer!
fileprivate var initialIndexPath: IndexPath?
fileprivate var currentLocationIndexPath: IndexPath?
fileprivate var draggingView: UIView?
fileprivate var scrollRate = 0.0
fileprivate var scrollDisplayLink: CADisplayLink?
fileprivate var feedbackGenerator: AnyObject?
fileprivate var previousGestureVerticalPosition: CGFloat?
/** A Bool property that indicates whether long press to reorder is enabled. */
open var longPressReorderEnabled: Bool {
get {
return longPressGestureRecognizer.isEnabled
}
set {
longPressGestureRecognizer.isEnabled = newValue
}
}
/**
The minimum period a finger must press on a cell for the reordering to begin.
The time interval is in seconds. The default duration is is 0.5 seconds.
*/
open var minimumPressDuration: CFTimeInterval {
get {
return longPressGestureRecognizer.minimumPressDuration
}
set {
longPressGestureRecognizer.minimumPressDuration = newValue
}
}
public convenience init() {
self.init(frame: CGRect.zero)
}
public override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
initialize()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
fileprivate func initialize() {
longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(LPRTableView._longPress(_:)))
addGestureRecognizer(longPressGestureRecognizer)
self.estimatedRowHeight = 0
self.estimatedSectionHeaderHeight = 0
self.estimatedSectionFooterHeight = 0
}
}
extension LPRTableView {
fileprivate func canMoveRowAt(indexPath: IndexPath) -> Bool {
return (dataSource?.responds(to: #selector(UITableViewDataSource.tableView(_:canMoveRowAt:))) == false) || (dataSource?.tableView?(self, canMoveRowAt: indexPath) == true)
}
fileprivate func cancelGesture() {
longPressGestureRecognizer.isEnabled = false
longPressGestureRecognizer.isEnabled = true
}
@objc internal func _longPress(_ gesture: UILongPressGestureRecognizer) {
let location = gesture.location(in: self)
let indexPath = indexPathForRow(at: location)
let sections = numberOfSections
var rows = 0
for i in 0..<sections {
rows += numberOfRows(inSection: i)
}
// Get out of here if the long press was not on a valid row or our table is empty
// or the dataSource tableView:canMoveRowAtIndexPath: doesn't allow moving the row.
if (rows == 0) ||
((gesture.state == UIGestureRecognizerState.began) && (indexPath == nil)) ||
((gesture.state == UIGestureRecognizerState.ended) && (currentLocationIndexPath == nil)) ||
((gesture.state == UIGestureRecognizerState.began) && !canMoveRowAt(indexPath: indexPath!)) {
cancelGesture()
return
}
// Started.
if gesture.state == .began {
self.hapticFeedbackSetup()
self.hapticFeedbackSelectionChanged()
self.previousGestureVerticalPosition = location.y
if let indexPath = indexPath {
if var cell = headerView(forSection: indexPath.section) {
// Create the view that will be dragged around the screen.
if (draggingView == nil) {
if let draggingCell = longPressReorderDelegate?.tableView?(self, draggingCell: cell, at: indexPath) {
cell = draggingCell
}
// Make an image from the pressed table view cell.
UIGraphicsBeginImageContextWithOptions(cell.bounds.size, false, 0.0)
cell.layer.render(in: UIGraphicsGetCurrentContext()!)
let cellImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
draggingView = UIImageView(image: cellImage)
if let draggingView = draggingView {
addSubview(draggingView)
let rect1 = rect(forSection: indexPath.section)
draggingView.frame = draggingView.bounds.offsetBy(dx: rect1.origin.x, dy: rect1.origin.y)
UIView.beginAnimations("LongPressReorder-ShowDraggingView", context: nil)
longPressReorderDelegate?.tableView?(self, showDraggingView: draggingView, at: indexPath)
UIView.commitAnimations()
// Add drop shadow to image and lower opacity.
draggingView.layer.masksToBounds = false
draggingView.layer.shadowColor = UIColor.black.cgColor
draggingView.layer.shadowOffset = CGSize.zero
draggingView.layer.shadowRadius = 4.0
draggingView.layer.shadowOpacity = 0.7
draggingView.layer.opacity = 0.85
// Zoom image towards user.
UIView.beginAnimations("LongPressReorder-Zoom", context: nil)
draggingView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location))
UIView.commitAnimations()
}
}
cell.isHidden = true
currentLocationIndexPath = indexPath
initialIndexPath = indexPath
// Enable scrolling for cell.
scrollDisplayLink = CADisplayLink(target: self, selector: #selector(LPRTableView._scrollTableWithCell(_:)))
scrollDisplayLink?.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
}
}
}
// Dragging.
else if gesture.state == .changed {
if let draggingView = draggingView {
// Update position of the drag view
draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location))
if let previousGestureVerticalPosition = self.previousGestureVerticalPosition {
if location.y != previousGestureVerticalPosition {
longPressReorderDelegate?.tableView?(self, draggingGestureChanged: gesture)
self.previousGestureVerticalPosition = location.y
}
} else {
longPressReorderDelegate?.tableView?(self, draggingGestureChanged: gesture)
self.previousGestureVerticalPosition = location.y
}
}
let inset: UIEdgeInsets
if #available(iOS 11.0, *) {
inset = adjustedContentInset
} else {
inset = contentInset
}
var rect = bounds
// Adjust rect for content inset, as we will use it below for calculating scroll zones.
rect.size.height -= inset.top
updateCurrentLocation(gesture)
// Tell us if we should scroll, and in which direction.
let scrollZoneHeight = rect.size.height / 6.0
let bottomScrollBeginning = contentOffset.y + inset.top + rect.size.height - scrollZoneHeight
let topScrollBeginning = contentOffset.y + inset.top + scrollZoneHeight
// We're in the bottom zone.
print("=== Location: \(location.y)")
if location.y >= bottomScrollBeginning {
scrollRate = Double(location.y - bottomScrollBeginning) / Double(scrollZoneHeight)
}
// We're in the top zone.
else if location.y <= topScrollBeginning {
scrollRate = Double(location.y - topScrollBeginning) / Double(scrollZoneHeight)
}
else {
scrollRate = 0.0
}
}
// Dropped.
else if (gesture.state == .ended) || (gesture.state == .cancelled) || (gesture.state == .failed) {
// Remove previously cached Gesture location
self.previousGestureVerticalPosition = nil
// Remove scrolling CADisplayLink.
scrollDisplayLink?.invalidate()
scrollDisplayLink = nil
scrollRate = 0.0
// Animate the drag view to the newly hovered cell.
UIView.animate(withDuration: 0.3,
animations: { [unowned self] () -> Void in
if let draggingView = self.draggingView {
if let currentLocationIndexPath = self.currentLocationIndexPath {
UIView.beginAnimations("LongPressReorder-HideDraggingView", context: nil)
self.longPressReorderDelegate?.tableView?(self, hideDraggingView: draggingView, at: currentLocationIndexPath)
UIView.commitAnimations()
let rect = self.rectForRow(at: currentLocationIndexPath)
draggingView.transform = CGAffineTransform.identity
draggingView.frame = draggingView.bounds.offsetBy(dx: rect.origin.x, dy: rect.origin.y)
}
}
},
completion: { [unowned self] (Bool) -> Void in
if let draggingView = self.draggingView {
draggingView.removeFromSuperview()
}
// Reload the rows that were affected just to be safe.
self.reloadData()
self.currentLocationIndexPath = nil
self.draggingView = nil
self.hapticFeedbackSelectionChanged()
self.hapticFeedbackFinalize()
})
}
}
fileprivate func updateCurrentLocation(_ gesture: UILongPressGestureRecognizer) {
let location = gesture.location(in: self)
guard var indexPath = indexPathForRow(at: location) else { return }
if let iIndexPath = initialIndexPath,
let ip = delegate?.tableView?(self, targetIndexPathForMoveFromRowAt: iIndexPath, toProposedIndexPath: indexPath) {
indexPath = ip
}
guard let clIndexPath = currentLocationIndexPath else { return }
let oldHeight = rectForRow(at: clIndexPath).size.height
let newHeight = rectForRow(at: indexPath).size.height
if let cell = headerView(forSection: clIndexPath.section) {
cell.isHidden = true
}
if ((indexPath != clIndexPath) && (gesture.location(in: headerView(forSection: indexPath.section)).y > (newHeight - oldHeight))) && canMoveRowAt(indexPath: indexPath) {
beginUpdates()
moveSection(clIndexPath.section, toSection: indexPath.section)
dataSource?.tableView?(self, moveRowAt: clIndexPath, to: indexPath)
currentLocationIndexPath = indexPath
endUpdates()
self.hapticFeedbackSelectionChanged()
}
}
@objc internal func _scrollTableWithCell(_ sender: CADisplayLink) {
guard let gesture = longPressGestureRecognizer else { return }
let location = gesture.location(in: self)
guard !(location.y.isNaN || location.x.isNaN) else { return } // Explicitly check for out-of-bound touch.
let yOffset = Double(contentOffset.y) + scrollRate * 10.0
var newOffset = CGPoint(x: contentOffset.x, y: CGFloat(yOffset))
let inset: UIEdgeInsets
if #available(iOS 11.0, *) {
inset = adjustedContentInset
} else {
inset = contentInset
}
if newOffset.y < -inset.top {
newOffset.y = -inset.top
} else if (contentSize.height + inset.bottom) < frame.size.height {
newOffset = contentOffset
} else if newOffset.y > ((contentSize.height + inset.bottom) - frame.size.height) {
newOffset.y = (contentSize.height + inset.bottom) - frame.size.height
}
contentOffset = newOffset
if let draggingView = draggingView {
draggingView.center = CGPoint(x: center.x, y: newYCenter(for: draggingView, with: location))
}
updateCurrentLocation(gesture)
}
fileprivate func newYCenter(for draggingView: UIView, with location: CGPoint) -> CGFloat {
let cellCenter = draggingView.frame.height / 2
let bottomBound = contentSize.height - cellCenter
if location.y < cellCenter {
return cellCenter
} else if location.y > bottomBound {
return bottomBound
}
return location.y
}
}
extension LPRTableView {
fileprivate func hapticFeedbackSetup() {
guard #available(iOS 10.0, *) else { return }
let feedbackGenerator = UISelectionFeedbackGenerator()
feedbackGenerator.prepare()
self.feedbackGenerator = feedbackGenerator
}
fileprivate func hapticFeedbackSelectionChanged() {
guard #available(iOS 10.0, *),
let feedbackGenerator = self.feedbackGenerator as? UISelectionFeedbackGenerator else { return }
feedbackGenerator.selectionChanged()
feedbackGenerator.prepare()
}
fileprivate func hapticFeedbackFinalize() {
guard #available(iOS 10.0, *) else { return }
self.feedbackGenerator = nil
}
}
Is it possible to implement this framework for re-ordering sections by getting indexPath from a selected point. I can manage to do it on my own without this framework but I am having difficulties with the scrolling so I was wondering if I could use this instead.
Even with this framework I have got it working to move the sections but I cannot implement the scrolling again.