SwipeController.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. //
  2. // SwipeController.swift
  3. // SwipeCellKit
  4. //
  5. // Created by Mohammad Kurabi on 5/19/18.
  6. //
  7. import Foundation
  8. import UIKit
  9. protocol SwipeControllerDelegate: class {
  10. func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool
  11. func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]?
  12. func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions
  13. func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation)
  14. func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation)
  15. func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath)
  16. func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect?
  17. }
  18. class SwipeController: NSObject {
  19. weak var swipeable: (UIView & Swipeable)?
  20. weak var actionsContainerView: UIView?
  21. weak var delegate: SwipeControllerDelegate?
  22. weak var scrollView: UIScrollView?
  23. var animator: SwipeAnimator?
  24. let elasticScrollRatio: CGFloat = 0.4
  25. var originalCenter: CGFloat = 0
  26. var scrollRatio: CGFloat = 1.0
  27. var originalLayoutMargins: UIEdgeInsets = .zero
  28. lazy var panGestureRecognizer: UIPanGestureRecognizer = {
  29. let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(gesture:)))
  30. gesture.delegate = self
  31. return gesture
  32. }()
  33. lazy var tapGestureRecognizer: UITapGestureRecognizer = {
  34. let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(gesture:)))
  35. gesture.delegate = self
  36. return gesture
  37. }()
  38. init(swipeable: UIView & Swipeable, actionsContainerView: UIView) {
  39. self.swipeable = swipeable
  40. self.actionsContainerView = actionsContainerView
  41. super.init()
  42. configure()
  43. }
  44. @objc func handlePan(gesture: UIPanGestureRecognizer) {
  45. guard let target = actionsContainerView, var swipeable = self.swipeable else { return }
  46. let velocity = gesture.velocity(in: target)
  47. if delegate?.swipeController(self, canBeginEditingSwipeableFor: velocity.x > 0 ? .left : .right) == false {
  48. return
  49. }
  50. switch gesture.state {
  51. case .began:
  52. if let swipeable = scrollView?.swipeables.first(where: { $0.state == .dragging }) as? UIView, self.swipeable != nil, swipeable != self.swipeable! {
  53. return
  54. }
  55. stopAnimatorIfNeeded()
  56. originalCenter = target.center.x
  57. if swipeable.state == .center || swipeable.state == .animatingToCenter {
  58. let orientation: SwipeActionsOrientation = velocity.x > 0 ? .left : .right
  59. showActionsView(for: orientation)
  60. }
  61. case .changed:
  62. guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return }
  63. guard swipeable.state.isActive else { return }
  64. if swipeable.state == .animatingToCenter {
  65. let swipedCell = scrollView?.swipeables.first(where: { $0.state == .dragging || $0.state == .left || $0.state == .right }) as? UIView
  66. if let swipedCell = swipedCell, self.swipeable != nil, swipedCell != self.swipeable! {
  67. return
  68. }
  69. }
  70. let translation = gesture.translation(in: target).x
  71. scrollRatio = 1.0
  72. // Check if dragging past the center of the opposite direction of action view, if so
  73. // then we need to apply elasticity
  74. if (translation + originalCenter - swipeable.bounds.midX) * actionsView.orientation.scale > 0 {
  75. target.center.x = gesture.elasticTranslation(in: target,
  76. withLimit: .zero,
  77. fromOriginalCenter: CGPoint(x: originalCenter, y: 0)).x
  78. swipeable.actionsView?.visibleWidth = abs((swipeable as Swipeable).frame.minX)
  79. scrollRatio = elasticScrollRatio
  80. return
  81. }
  82. if let expansionStyle = actionsView.options.expansionStyle, let scrollView = scrollView {
  83. let referenceFrame = actionsContainerView != swipeable ? actionsContainerView.frame : nil;
  84. let expanded = expansionStyle.shouldExpand(view: swipeable, gesture: gesture, in: scrollView, within: referenceFrame)
  85. let targetOffset = expansionStyle.targetOffset(for: swipeable)
  86. let currentOffset = abs(translation + originalCenter - swipeable.bounds.midX)
  87. if expanded && !actionsView.expanded && targetOffset > currentOffset {
  88. let centerForTranslationToEdge = swipeable.bounds.midX - targetOffset * actionsView.orientation.scale
  89. let delta = centerForTranslationToEdge - originalCenter
  90. animate(toOffset: centerForTranslationToEdge)
  91. gesture.setTranslation(CGPoint(x: delta, y: 0), in: swipeable.superview!)
  92. } else {
  93. target.center.x = gesture.elasticTranslation(in: target,
  94. withLimit: CGSize(width: targetOffset, height: 0),
  95. fromOriginalCenter: CGPoint(x: originalCenter, y: 0),
  96. applyingRatio: expansionStyle.targetOverscrollElasticity).x
  97. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  98. }
  99. actionsView.setExpanded(expanded: expanded, feedback: true)
  100. } else {
  101. target.center.x = gesture.elasticTranslation(in: target,
  102. withLimit: CGSize(width: actionsView.preferredWidth, height: 0),
  103. fromOriginalCenter: CGPoint(x: originalCenter, y: 0),
  104. applyingRatio: elasticScrollRatio).x
  105. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  106. if (target.center.x - originalCenter) / translation != 1.0 {
  107. scrollRatio = elasticScrollRatio
  108. }
  109. }
  110. case .ended, .cancelled, .failed:
  111. guard let actionsView = swipeable.actionsView, let actionsContainerView = self.actionsContainerView else { return }
  112. if swipeable.state.isActive == false && swipeable.bounds.midX == target.center.x {
  113. return
  114. }
  115. swipeable.state = targetState(forVelocity: velocity)
  116. if actionsView.expanded == true, let expandedAction = actionsView.expandableAction {
  117. perform(action: expandedAction)
  118. } else {
  119. let targetOffset = targetCenter(active: swipeable.state.isActive)
  120. let distance = targetOffset - actionsContainerView.center.x
  121. let normalizedVelocity = velocity.x * scrollRatio / distance
  122. animate(toOffset: targetOffset, withInitialVelocity: normalizedVelocity) { _ in
  123. if self.swipeable?.state == .center {
  124. self.reset()
  125. }
  126. }
  127. if !swipeable.state.isActive {
  128. delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation)
  129. }
  130. }
  131. default: break
  132. }
  133. }
  134. @discardableResult
  135. func showActionsView(for orientation: SwipeActionsOrientation) -> Bool {
  136. guard let actions = delegate?.swipeController(self, editActionsForSwipeableFor: orientation), actions.count > 0 else { return false }
  137. guard let swipeable = self.swipeable else { return false }
  138. originalLayoutMargins = swipeable.layoutMargins
  139. configureActionsView(with: actions, for: orientation)
  140. delegate?.swipeController(self, willBeginEditingSwipeableFor: orientation)
  141. return true
  142. }
  143. func configureActionsView(with actions: [SwipeAction], for orientation: SwipeActionsOrientation) {
  144. guard var swipeable = self.swipeable,
  145. let actionsContainerView = self.actionsContainerView,
  146. let scrollView = self.scrollView else {
  147. return
  148. }
  149. let options = delegate?.swipeController(self, editActionsOptionsForSwipeableFor: orientation) ?? SwipeOptions()
  150. swipeable.actionsView?.removeFromSuperview()
  151. swipeable.actionsView = nil
  152. var contentEdgeInsets = UIEdgeInsets.zero
  153. if let visibleTableViewRect = delegate?.swipeController(self, visibleRectFor: scrollView) {
  154. let frame = (swipeable as Swipeable).frame
  155. let visibleSwipeableRect = frame.intersection(visibleTableViewRect)
  156. if visibleSwipeableRect.isNull == false {
  157. let top = visibleSwipeableRect.minY > frame.minY ? max(0, visibleSwipeableRect.minY - frame.minY) : 0
  158. let bottom = max(0, frame.size.height - visibleSwipeableRect.size.height - top)
  159. contentEdgeInsets = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
  160. }
  161. }
  162. let actionsView = SwipeActionsView(contentEdgeInsets: contentEdgeInsets,
  163. maxSize: swipeable.bounds.size,
  164. safeAreaInsetView: scrollView,
  165. options: options,
  166. orientation: orientation,
  167. actions: actions)
  168. actionsView.delegate = self
  169. actionsContainerView.addSubview(actionsView)
  170. actionsView.heightAnchor.constraint(equalTo: swipeable.heightAnchor).isActive = true
  171. actionsView.widthAnchor.constraint(equalTo: swipeable.widthAnchor, multiplier: 2).isActive = true
  172. actionsView.topAnchor.constraint(equalTo: swipeable.topAnchor).isActive = true
  173. if orientation == .left {
  174. actionsView.rightAnchor.constraint(equalTo: actionsContainerView.leftAnchor).isActive = true
  175. } else {
  176. actionsView.leftAnchor.constraint(equalTo: actionsContainerView.rightAnchor).isActive = true
  177. }
  178. actionsView.setNeedsUpdateConstraints()
  179. swipeable.actionsView = actionsView
  180. swipeable.state = .dragging
  181. }
  182. func animate(duration: Double = 0.7, toOffset offset: CGFloat, withInitialVelocity velocity: CGFloat = 0, completion: ((Bool) -> Void)? = nil) {
  183. stopAnimatorIfNeeded()
  184. swipeable?.layoutIfNeeded()
  185. let animator: SwipeAnimator = {
  186. if velocity != 0 {
  187. if #available(iOS 10, *) {
  188. let velocity = CGVector(dx: velocity, dy: velocity)
  189. let parameters = UISpringTimingParameters(mass: 1.0, stiffness: 100, damping: 18, initialVelocity: velocity)
  190. return UIViewPropertyAnimator(duration: 0.0, timingParameters: parameters)
  191. } else {
  192. return UIViewSpringAnimator(duration: duration, damping: 1.0, initialVelocity: velocity)
  193. }
  194. } else {
  195. if #available(iOS 10, *) {
  196. return UIViewPropertyAnimator(duration: duration, dampingRatio: 1.0)
  197. } else {
  198. return UIViewSpringAnimator(duration: duration, damping: 1.0)
  199. }
  200. }
  201. }()
  202. animator.addAnimations({
  203. guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
  204. actionsContainerView.center = CGPoint(x: offset, y: actionsContainerView.center.y)
  205. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  206. swipeable.layoutIfNeeded()
  207. })
  208. if let completion = completion {
  209. animator.addCompletion(completion: completion)
  210. }
  211. self.animator = animator
  212. animator.startAnimation()
  213. }
  214. func traitCollectionDidChange(from previousTraitCollrection: UITraitCollection?, to traitCollection: UITraitCollection) {
  215. guard let swipeable = self.swipeable,
  216. let actionsContainerView = self.actionsContainerView,
  217. previousTraitCollrection != nil else {
  218. return
  219. }
  220. if swipeable.state == .left || swipeable.state == .right {
  221. let targetOffset = targetCenter(active: swipeable.state.isActive)
  222. actionsContainerView.center = CGPoint(x: targetOffset, y: actionsContainerView.center.y)
  223. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  224. swipeable.layoutIfNeeded()
  225. }
  226. }
  227. func stopAnimatorIfNeeded() {
  228. if animator?.isRunning == true {
  229. animator?.stopAnimation(true)
  230. }
  231. }
  232. @objc func handleTap(gesture: UITapGestureRecognizer) {
  233. hideSwipe(animated: true)
  234. }
  235. @objc func handleTablePan(gesture: UIPanGestureRecognizer) {
  236. if gesture.state == .began {
  237. hideSwipe(animated: true)
  238. }
  239. }
  240. func targetState(forVelocity velocity: CGPoint) -> SwipeState {
  241. guard let actionsView = swipeable?.actionsView else { return .center }
  242. switch actionsView.orientation {
  243. case .left:
  244. return (velocity.x < 0 && !actionsView.expanded) ? .center : .left
  245. case .right:
  246. return (velocity.x > 0 && !actionsView.expanded) ? .center : .right
  247. }
  248. }
  249. func targetCenter(active: Bool) -> CGFloat {
  250. guard let swipeable = self.swipeable else { return 0 }
  251. guard let actionsView = swipeable.actionsView, active == true else { return swipeable.bounds.midX }
  252. return swipeable.bounds.midX - actionsView.preferredWidth * actionsView.orientation.scale
  253. }
  254. func configure() {
  255. swipeable?.addGestureRecognizer(tapGestureRecognizer)
  256. swipeable?.addGestureRecognizer(panGestureRecognizer)
  257. }
  258. func reset() {
  259. swipeable?.state = .center
  260. swipeable?.actionsView?.removeFromSuperview()
  261. swipeable?.actionsView = nil
  262. }
  263. }
  264. extension SwipeController: UIGestureRecognizerDelegate {
  265. func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  266. if gestureRecognizer == tapGestureRecognizer {
  267. if UIAccessibility.isVoiceOverRunning {
  268. scrollView?.hideSwipeables()
  269. }
  270. let swipedCell = scrollView?.swipeables.first(where: {
  271. $0.state.isActive ||
  272. $0.panGestureRecognizer.state == .began ||
  273. $0.panGestureRecognizer.state == .changed ||
  274. $0.panGestureRecognizer.state == .ended
  275. })
  276. return swipedCell == nil ? false : true
  277. }
  278. if gestureRecognizer == panGestureRecognizer,
  279. let view = gestureRecognizer.view,
  280. let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
  281. let translation = gestureRecognizer.translation(in: view)
  282. return abs(translation.y) <= abs(translation.x)
  283. }
  284. return true
  285. }
  286. }
  287. extension SwipeController: SwipeActionsViewDelegate {
  288. func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction) {
  289. perform(action: action)
  290. }
  291. func perform(action: SwipeAction) {
  292. guard let actionsView = swipeable?.actionsView else { return }
  293. if action == actionsView.expandableAction, let expansionStyle = actionsView.options.expansionStyle {
  294. // Trigger the expansion (may already be expanded from drag)
  295. actionsView.setExpanded(expanded: true)
  296. switch expansionStyle.completionAnimation {
  297. case .bounce:
  298. perform(action: action, hide: true)
  299. case .fill(let fillOption):
  300. performFillAction(action: action, fillOption: fillOption)
  301. }
  302. } else {
  303. perform(action: action, hide: action.hidesWhenSelected)
  304. }
  305. }
  306. func perform(action: SwipeAction, hide: Bool) {
  307. guard let indexPath = swipeable?.indexPath else { return }
  308. if hide {
  309. hideSwipe(animated: true)
  310. }
  311. action.handler?(action, indexPath)
  312. }
  313. func performFillAction(action: SwipeAction, fillOption: SwipeExpansionStyle.FillOptions) {
  314. guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
  315. guard let actionsView = swipeable.actionsView, let indexPath = swipeable.indexPath else { return }
  316. let newCenter = swipeable.bounds.midX - (swipeable.bounds.width + actionsView.minimumButtonWidth) * actionsView.orientation.scale
  317. action.completionHandler = { [weak self] style in
  318. guard let `self` = self else { return }
  319. action.completionHandler = nil
  320. self.delegate?.swipeController(self, didEndEditingSwipeableFor: actionsView.orientation)
  321. switch style {
  322. case .delete:
  323. actionsContainerView.mask = actionsView.createDeletionMask()
  324. self.delegate?.swipeController(self, didDeleteSwipeableAt: indexPath)
  325. UIView.animate(withDuration: 0.3, animations: {
  326. guard let actionsContainerView = self.actionsContainerView else { return }
  327. actionsContainerView.center.x = newCenter
  328. actionsContainerView.mask?.frame.size.height = 0
  329. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  330. if fillOption.timing == .after {
  331. actionsView.alpha = 0
  332. }
  333. }) { [weak self] _ in
  334. self?.actionsContainerView?.mask = nil
  335. self?.resetSwipe()
  336. self?.reset()
  337. }
  338. case .reset:
  339. self.hideSwipe(animated: true)
  340. }
  341. }
  342. let invokeAction = {
  343. action.handler?(action, indexPath)
  344. if let style = fillOption.autoFulFillmentStyle {
  345. action.fulfill(with: style)
  346. }
  347. }
  348. animate(duration: 0.3, toOffset: newCenter) { _ in
  349. if fillOption.timing == .after {
  350. invokeAction()
  351. }
  352. }
  353. if fillOption.timing == .with {
  354. invokeAction()
  355. }
  356. }
  357. func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) {
  358. guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
  359. guard swipeable.state == .left || swipeable.state == .right else { return }
  360. guard let actionView = swipeable.actionsView else { return }
  361. swipeable.state = .animatingToCenter
  362. let targetCenter = self.targetCenter(active: false)
  363. if animated {
  364. animate(toOffset: targetCenter) { complete in
  365. self.reset()
  366. completion?(complete)
  367. }
  368. } else {
  369. actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y)
  370. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  371. reset()
  372. }
  373. delegate?.swipeController(self, didEndEditingSwipeableFor: actionView.orientation)
  374. }
  375. func resetSwipe() {
  376. guard let swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
  377. let targetCenter = self.targetCenter(active: false)
  378. actionsContainerView.center = CGPoint(x: targetCenter, y: actionsContainerView.center.y)
  379. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  380. }
  381. func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) {
  382. setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1,
  383. animated: animated,
  384. completion: completion)
  385. }
  386. func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) {
  387. guard var swipeable = self.swipeable, let actionsContainerView = self.actionsContainerView else { return }
  388. guard offset != 0 else {
  389. hideSwipe(animated: animated, completion: completion)
  390. return
  391. }
  392. let orientation: SwipeActionsOrientation = offset > 0 ? .left : .right
  393. let targetState = SwipeState(orientation: orientation)
  394. if swipeable.state != targetState {
  395. guard showActionsView(for: orientation) else { return }
  396. scrollView?.hideSwipeables()
  397. swipeable.state = targetState
  398. }
  399. let maxOffset = min(swipeable.bounds.width, abs(offset)) * orientation.scale * -1
  400. let targetCenter = abs(offset) == CGFloat.greatestFiniteMagnitude ? self.targetCenter(active: true) : swipeable.bounds.midX + maxOffset
  401. if animated {
  402. animate(toOffset: targetCenter) { complete in
  403. completion?(complete)
  404. }
  405. } else {
  406. actionsContainerView.center.x = targetCenter
  407. swipeable.actionsView?.visibleWidth = abs(actionsContainerView.frame.minX)
  408. }
  409. }
  410. }