LPRTableView icon indicating copy to clipboard operation
LPRTableView copied to clipboard

Implement LPR For TableView Sections?

Open RushanB opened this issue 7 years ago • 2 comments

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.

RushanB avatar Apr 14 '18 02:04 RushanB

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)?

nicolasgomollon avatar Apr 14 '18 12:04 nicolasgomollon

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
	}
}

RushanB avatar Apr 14 '18 18:04 RushanB