Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LPR For TableView Sections? #24

Open
RushanB opened this issue Apr 14, 2018 · 2 comments
Open

Implement LPR For TableView Sections? #24

RushanB opened this issue Apr 14, 2018 · 2 comments

Comments

@RushanB
Copy link

RushanB commented Apr 14, 2018

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.

@nicolasgomollon
Copy link
Owner

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

@RushanB
Copy link
Author

RushanB commented Apr 14, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants