123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- //
- // SwipeController.swift
- // SwipeCellKit
- //
- // Created by Mohammad Kurabi on 5/19/18.
- //
- import Foundation
- import UIKit
- protocol SwipeControllerDelegate: class {
-
- func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool
-
- func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]?
-
- func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions
-
- func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation)
-
- func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation)
-
- func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath)
-
- func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect?
-
- }
- class SwipeController: NSObject {
-
- weak var swipeable: (UIView & Swipeable)?
- weak var actionsContainerView: UIView?
-
- weak var delegate: SwipeControllerDelegate?
- weak var scrollView: UIScrollView?
-
- var animator: SwipeAnimator?
-
- let elasticScrollRatio: CGFloat = 0.4
-
- var originalCenter: CGFloat = 0
- var scrollRatio: CGFloat = 1.0
- var originalLayoutMargins: UIEdgeInsets = .zero
-
- lazy var panGestureRecognizer: UIPanGestureRecognizer = {
- let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
- gesture.delegate = self
- return gesture
- }()
-
- lazy var tapGestureRecognizer: UITapGestureRecognizer = {
- let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(gesture:)))
- gesture.delegate = self
- return gesture
- }()
-
- init(swipeable: UIView & Swipeable, actionsContainerView: UIView) {
- self.swipeable = swipeable
- self.actionsContainerView = actionsContainerView
-
- super.init()
-
- configure()
- }
- @objc func handlePan(gesture: UIPanGestureRecognizer) {
- guard let target = actionsContainerView, var swipeable = self.swipeable else { return }
-
- let velocity = gesture.velocity(in: target)
-
- if delegate?.swipeController(self, canBeginEditingSwipeableFor: velocity.x > 0 ? .left : .right) == false {
- return
- }
-
- switch gesture.state {
- case .began:
- if let swipeable = scrollView?.swipeables.first(where: { $0.state == .dragging }) as? UIView, self.swipeable != nil, swipeable != self.swipeable! {
- return
- }
-
- stopAnimatorIfNeeded()
-
- originalCenter = target.center.x
-
- if swipeable.state == .center || swipeable.state == .animatingToCenter {
- let orientation: SwipeActionsOrientation = velocity.x > 0 ? .left : .right
-
- showActionsView(for: orientation)
- }
- case .changed:
- guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return }
- guard swipeable.state.isActive else { return }
-
- if swipeable.state == .animatingToCenter {
- let swipedCell = scrollView?.swipeables.first(where: { $0.state == .dragging || $0.state == .left || $0.state == .right }) as? UIView
- if let swipedCell = swipedCell, self.swipeable != nil, swipedCell != self.swipeable! {
- return
- }
- }
-
- let translation = gesture.translation(in: target).x
- scrollRatio = 1.0
-
- // Check if dragging past the center of the opposite direction of action view, if so
- // then we need to apply elasticity
- if (translation + originalCenter - swipeable.bounds.midX) * actionsView.orientation.scale > 0 {
- target.center.x = gesture.elasticTranslation(in: target,
- withLimit: .zero,
- fromOriginalCenter: CGPoint(x: originalCenter, y: 0)).x
- swipeable.actionsView?.visibleWidth = abs((swipeable as Swipeable).frame.minX)
- scrollRatio = elasticScrollRatio
- return
- }
-
- if let expansionStyle = actionsView.options.expansionStyle, let scrollView = scrollView {
-
- let referenceFrame = actionsContainerView != swipeable ? actionsContainerView.frame : nil;
- let expanded = expansionStyle.shouldExpand(view: swipeable, gesture: gesture, in: scrollView, within: referenceFrame)
- let targetOffset = expansionStyle.targetOffset(for: swipeable)
- let currentOffset = abs(translation + originalCenter - swipeable.bounds.midX)
-
- if expanded && !actionsView.expanded && targetOffset > currentOffset {
- let centerForTranslationToEdge = swipeable.bounds.midX - targetOffset * actionsView.orientation.scale
- let delta = centerForTranslationToEdge - originalCenter
-
- animate(toOffset: centerForTranslationToEdge)
- gesture.setTranslation(CGPoint(x: delta, y: 0), in: swipeable.superview!)
- } else {
- target.center.x = gesture.elasticTranslation(in: target,
- withLimit: CGSize(width: targetOffset, height: 0),
- fromOriginalCenter: CGPoint(x: originalCenter, y: 0),
- applyingRatio: expansionStyle.targetOverscrollElasticity).x
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- }
-
- actionsView.setExpanded(expanded: expanded, feedback: true)
- } else {
- target.center.x = gesture.elasticTranslation(in: target,
- withLimit: CGSize(width: actionsView.preferredWidth, height: 0),
- fromOriginalCenter: CGPoint(x: originalCenter, y: 0),
- applyingRatio: elasticScrollRatio).x
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
-
- if (target.center.x - originalCenter) / translation != 1.0 {
- scrollRatio = elasticScrollRatio
- }
- }
- case .ended, .cancelled, .failed:
- guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return }
- if swipeable.state.isActive == false && swipeable.bounds.midX == target.center.x {
- return
- }
-
- swipeable.state = targetState(forVelocity: velocity)
-
- if actionsView.expanded == true, let expandedAction = actionsView.expandableAction {
- perform(action: expandedAction)
- } else {
- let targetOffset = targetCenter(active: swipeable.state.isActive)
- let distance = targetOffset - actionsContainerView.center.x
- let normalizedVelocity = velocity.x * scrollRatio / distance
-
- animate(toOffset: targetOffset, withInitialVelocity: normalizedVelocity) { _ in
- if self.swipeable?.state == .center {
- self.reset()
- }
- }
-
- if !swipeable.state.isActive {
- delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation)
- }
- }
- default: break
- }
- }
-
- @discardableResult
- func showActionsView(for orientation: SwipeActionsOrientation) -> Bool {
- guard let actions = delegate?.swipeController(self, editActionsForSwipeableFor: orientation), actions.count > 0 else { return false }
- guard let swipeable = self.swipeable else { return false }
-
- originalLayoutMargins = swipeable.layoutMargins
-
- configureActionsView(with: actions, for: orientation)
-
- delegate?.swipeController(self, willBeginEditingSwipeableFor: orientation)
-
- return true
- }
-
- func configureActionsView(with actions: [SwipeAction], for orientation: SwipeActionsOrientation) {
- guard var swipeable = self.swipeable,
- let actionsContainerView = self.actionsContainerView,
- let scrollView = self.scrollView else {
- return
- }
- let options = delegate?.swipeController(self, editActionsOptionsForSwipeableFor: orientation) ?? SwipeOptions()
-
- swipeable.actionsView?.removeFromSuperview()
- swipeable.actionsView = nil
-
- var contentEdgeInsets = UIEdgeInsets.zero
- if let visibleTableViewRect = delegate?.swipeController(self, visibleRectFor: scrollView) {
-
- let frame = (swipeable as Swipeable).frame
- let visibleSwipeableRect = frame.intersection(visibleTableViewRect)
- if visibleSwipeableRect.isNull == false {
- let top = visibleSwipeableRect.minY > frame.minY ? max(0, visibleSwipeableRect.minY - frame.minY) : 0
- let bottom = max(0, frame.size.height - visibleSwipeableRect.size.height - top)
- contentEdgeInsets = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
- }
- }
-
- let actionsView = SwipeActionsView(contentEdgeInsets: contentEdgeInsets,
- maxSize: swipeable.bounds.size,
- safeAreaInsetView: scrollView,
- options: options,
- orientation: orientation,
- actions: actions)
- actionsView.delegate = self
-
- actionsContainerView.addSubview(actionsView)
-
- actionsView.heightAnchor.constraint(equalTo: swipeable.heightAnchor).isActive = true
- actionsView.widthAnchor.constraint(equalTo: swipeable.widthAnchor, multiplier: 2).isActive = true
- actionsView.topAnchor.constraint(equalTo: swipeable.topAnchor).isActive = true
-
- if orientation == .left {
- actionsView.rightAnchor.constraint(equalTo: actionsContainerView.leftAnchor).isActive = true
- } else {
- actionsView.leftAnchor.constraint(equalTo: actionsContainerView.rightAnchor).isActive = true
- }
-
- actionsView.setNeedsUpdateConstraints()
-
- swipeable.actionsView = actionsView
-
- swipeable.state = .dragging
- }
-
- func animate(duration: Double = 0.7, toOffset offset: CGFloat, withInitialVelocity velocity: CGFloat = 0, completion: ((Bool) -> Void)? = nil) {
- stopAnimatorIfNeeded()
-
- swipeable?.layoutIfNeeded()
-
- let animator: SwipeAnimator = {
- if velocity != 0 {
- if #available(iOS 10, *) {
- let velocity = CGVector(dx: velocity, dy: velocity)
- let parameters = UISpringTimingParameters(mass: 1.0, stiffness: 100, damping: 18, initialVelocity: velocity)
- return UIViewPropertyAnimator(duration: 0.0, timingParameters: parameters)
- } else {
- return UIViewSpringAnimator(duration: duration, damping: 1.0, initialVelocity: velocity)
- }
- } else {
- if #available(iOS 10, *) {
- return UIViewPropertyAnimator(duration: duration, dampingRatio: 1.0)
- } else {
- return UIViewSpringAnimator(duration: duration, damping: 1.0)
- }
- }
- }()
-
- animator.addAnimations({
- guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
-
- actionsContainerView.center = CGPoint(x: offset, y: actionsContainerView.center.y)
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- swipeable.layoutIfNeeded()
- })
-
- if let completion = completion {
- animator.addCompletion(completion: completion)
- }
-
- self.animator = animator
-
- animator.startAnimation()
- }
-
- func traitCollectionDidChange(from previousTraitCollrection: UITraitCollection?, to traitCollection: UITraitCollection) {
- guard let swipeable = self.swipeable,
- let actionsContainerView = self.actionsContainerView,
- previousTraitCollrection != nil else {
- return
- }
-
- if swipeable.state == .left || swipeable.state == .right {
- let targetOffset = targetCenter(active: swipeable.state.isActive)
- actionsContainerView.center = CGPoint(x: targetOffset, y: actionsContainerView.center.y)
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- swipeable.layoutIfNeeded()
- }
- }
-
- func stopAnimatorIfNeeded() {
- if animator?.isRunning == true {
- animator?.stopAnimation(true)
- }
- }
-
- @objc func handleTap(gesture: UITapGestureRecognizer) {
- hideSwipe(animated: true)
- }
-
- @objc func handleTablePan(gesture: UIPanGestureRecognizer) {
- if gesture.state == .began {
- hideSwipe(animated: true)
- }
- }
-
- func targetState(forVelocity velocity: CGPoint) -> SwipeState {
- guard let actionsView = swipeable?.actionsView else { return .center }
-
- switch actionsView.orientation {
- case .left:
- return (velocity.x < 0 && !actionsView.expanded) ? .center : .left
- case .right:
- return (velocity.x > 0 && !actionsView.expanded) ? .center : .right
- }
- }
-
- func targetCenter(active: Bool) -> CGFloat {
- guard let swipeable = self.swipeable else { return 0 }
- guard let actionsView = swipeable.actionsView, active == true else { return swipeable.bounds.midX }
-
- return swipeable.bounds.midX - actionsView.preferredWidth * actionsView.orientation.scale
- }
-
- func configure() {
- swipeable?.addGestureRecognizer(tapGestureRecognizer)
- swipeable?.addGestureRecognizer(panGestureRecognizer)
- }
-
- func reset() {
- swipeable?.state = .center
-
- swipeable?.actionsView?.removeFromSuperview()
- swipeable?.actionsView = nil
- }
-
- }
- extension SwipeController: UIGestureRecognizerDelegate {
- func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- if gestureRecognizer == tapGestureRecognizer {
- if UIAccessibility.isVoiceOverRunning {
- scrollView?.hideSwipeables()
- }
-
- let swipedCell = scrollView?.swipeables.first(where: {
- $0.state.isActive ||
- $0.panGestureRecognizer.state == .began ||
- $0.panGestureRecognizer.state == .changed ||
- $0.panGestureRecognizer.state == .ended
- })
- return swipedCell == nil ? false : true
- }
-
- if gestureRecognizer == panGestureRecognizer,
- let view = gestureRecognizer.view,
- let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
- let translation = gestureRecognizer.translation(in: view)
- return abs(translation.y) <= abs(translation.x)
- }
-
- return true
- }
- }
- extension SwipeController: SwipeActionsViewDelegate {
- func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction) {
- perform(action: action)
- }
-
- func perform(action: SwipeAction) {
- guard let actionsView = swipeable?.actionsView else { return }
-
- if action == actionsView.expandableAction, let expansionStyle = actionsView.options.expansionStyle {
- // Trigger the expansion (may already be expanded from drag)
- actionsView.setExpanded(expanded: true)
-
- switch expansionStyle.completionAnimation {
- case .bounce:
- perform(action: action, hide: true)
- case .fill(let fillOption):
- performFillAction(action: action, fillOption: fillOption)
- }
- } else {
- perform(action: action, hide: action.hidesWhenSelected)
- }
- }
-
- func perform(action: SwipeAction, hide: Bool) {
- guard let indexPath = swipeable?.indexPath else { return }
- if hide {
- hideSwipe(animated: true)
- }
- action.handler?(action, indexPath)
- }
-
- func performFillAction(action: SwipeAction, fillOption: SwipeExpansionStyle.FillOptions) {
- guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
- guard let actionsView = swipeable.actionsView, let indexPath = swipeable.indexPath else { return }
- let newCenter = swipeable.bounds.midX - (swipeable.bounds.width + actionsView.minimumButtonWidth) * actionsView.orientation.scale
-
- action.completionHandler = { [weak self] style in
- guard let `self` = self else { return }
- action.completionHandler = nil
-
- self.delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation)
-
- switch style {
- case .delete:
- actionsContainerView.mask = actionsView.createDeletionMask()
-
- self.delegate?.swipeController(self, didDeleteSwipeableAt: indexPath)
-
- UIView.animate(withDuration: 0.3, animations: {
- guard let actionsContainerView = self.actionsContainerView else { return }
-
- actionsContainerView.center.x = newCenter
- actionsContainerView.mask?.frame.size.height = 0
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
-
- if fillOption.timing == .after {
- actionsView.alpha = 0
- }
- }) { [weak self] _ in
- self?.actionsContainerView?.mask = nil
- self?.resetSwipe()
- self?.reset()
- }
- case .reset:
- self.hideSwipe(animated: true)
- }
- }
-
- let invokeAction = {
- action.handler?(action, indexPath)
-
- if let style = fillOption.autoFulFillmentStyle {
- action.fulfill(with: style)
- }
- }
-
- animate(duration: 0.3, toOffset: newCenter) { _ in
- if fillOption.timing == .after {
- invokeAction()
- }
- }
-
- if fillOption.timing == .with {
- invokeAction()
- }
- }
-
- func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) {
- guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
- guard swipeable.state == .left || swipeable.state == .right else { return }
- guard let actionView = swipeable.actionsView else { return }
-
- swipeable.state = .animatingToCenter
-
- let targetCenter = self.targetCenter(active: false)
-
- if animated {
- animate(toOffset: targetCenter) { complete in
- self.reset()
- completion?(complete)
- }
- } else {
- actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y)
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- reset()
- }
-
- delegate?.swipeController(self, didEndEditingSwipeableFor: actionView.orientation)
- }
-
- func resetSwipe() {
- guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
-
- let targetCenter = self.targetCenter(active: false)
-
- actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y)
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- }
-
- func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) {
- setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1,
- animated: animated,
- completion: completion)
- }
-
- func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) {
- guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
-
- guard offset != 0 else {
- hideSwipe(animated: animated, completion: completion)
- return
- }
-
- let orientation: SwipeActionsOrientation = offset > 0 ? .left : .right
- let targetState = SwipeState(orientation: orientation)
-
- if swipeable.state != targetState {
- guard showActionsView(for: orientation) else { return }
-
- scrollView?.hideSwipeables()
-
- swipeable.state = targetState
- }
-
- let maxOffset = min(swipeable.bounds.width, abs(offset)) * orientation.scale * -1
- let targetCenter = abs(offset) == CGFloat.greatestFiniteMagnitude ? self.targetCenter(active: true) : swipeable.bounds.midX + maxOffset
-
- if animated {
- animate(toOffset: targetCenter) { complete in
- completion?(complete)
- }
- } else {
- actionsContainerView.center.x = targetCenter
- swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
- }
- }
- }
|