SwipeCollectionViewCell.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. //
  2. // SwipeCollectionViewCell.swift
  3. // SwipeCellKit
  4. //
  5. // Created by Jeremy Koch
  6. // Copyright © 2017 Jeremy Koch. All rights reserved.
  7. //
  8. import UIKit
  9. /**
  10. The `SwipeCollectionViewCell` class extends `UICollectionViewCell` and provides more flexible options for cell swiping behavior.
  11. The default behavior closely matches the stock Mail.app. If you want to customize the transition style (ie. how the action buttons are exposed), or the expansion style (the behavior when the row is swiped passes a defined threshold), you can return the appropriately configured `SwipeOptions` via the `SwipeCollectionViewCellDelegate` delegate.
  12. */
  13. open class SwipeCollectionViewCell: UICollectionViewCell {
  14. /// The object that acts as the delegate of the `SwipeCollectionViewCell`.
  15. public weak var delegate: SwipeCollectionViewCellDelegate?
  16. var state = SwipeState.center
  17. var actionsView: SwipeActionsView?
  18. var scrollView: UIScrollView? {
  19. return collectionView
  20. }
  21. var indexPath: IndexPath? {
  22. return collectionView?.indexPath(for: self)
  23. }
  24. var panGestureRecognizer: UIGestureRecognizer
  25. {
  26. return swipeController.panGestureRecognizer;
  27. }
  28. var swipeController: SwipeController!
  29. var isPreviouslySelected = false
  30. weak var collectionView: UICollectionView?
  31. /// :nodoc:
  32. open override var frame: CGRect {
  33. set { super.frame = state.isActive ? CGRect(origin: CGPoint(x: frame.minX, y: newValue.minY), size: newValue.size) : newValue }
  34. get { return super.frame }
  35. }
  36. /// :nodoc:
  37. open override var isHighlighted: Bool {
  38. set {
  39. guard state == .center else { return }
  40. super.isHighlighted = newValue
  41. }
  42. get { return super.isHighlighted }
  43. }
  44. /// :nodoc:
  45. open override var layoutMargins: UIEdgeInsets {
  46. get {
  47. return frame.origin.x != 0 ? swipeController.originalLayoutMargins : super.layoutMargins
  48. }
  49. set {
  50. super.layoutMargins = newValue
  51. }
  52. }
  53. /// :nodoc:
  54. override public init(frame: CGRect) {
  55. super.init(frame: frame)
  56. configure()
  57. }
  58. /// :nodoc:
  59. required public init?(coder aDecoder: NSCoder) {
  60. super.init(coder: aDecoder)
  61. configure()
  62. }
  63. deinit {
  64. collectionView?.panGestureRecognizer.removeTarget(self, action: nil)
  65. }
  66. func configure() {
  67. contentView.clipsToBounds = false
  68. if contentView.translatesAutoresizingMaskIntoConstraints == true {
  69. contentView.translatesAutoresizingMaskIntoConstraints = false
  70. NSLayoutConstraint.activate([
  71. contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
  72. contentView.topAnchor.constraint(equalTo: self.topAnchor),
  73. contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
  74. contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
  75. ])
  76. }
  77. swipeController = SwipeController(swipeable: self, actionsContainerView: contentView)
  78. swipeController.delegate = self
  79. }
  80. /// :nodoc:
  81. override open func prepareForReuse() {
  82. super.prepareForReuse()
  83. reset()
  84. resetSelectedState()
  85. }
  86. /// :nodoc:
  87. override open func didMoveToSuperview() {
  88. super.didMoveToSuperview()
  89. var view: UIView = self
  90. while let superview = view.superview {
  91. view = superview
  92. if let collectionView = view as? UICollectionView {
  93. self.collectionView = collectionView
  94. swipeController.scrollView = scrollView
  95. collectionView.panGestureRecognizer.removeTarget(self, action: nil)
  96. collectionView.panGestureRecognizer.addTarget(self, action: #selector(handleCollectionPan(gesture:)))
  97. return
  98. }
  99. }
  100. }
  101. /// :nodoc:
  102. open override func willMove(toWindow newWindow: UIWindow?) {
  103. super.willMove(toWindow: newWindow)
  104. if newWindow == nil {
  105. reset()
  106. }
  107. }
  108. // Override so we can accept touches anywhere within the cell's original frame.
  109. // This is required to detect touches on the `SwipeActionsView` sitting alongside the
  110. // `SwipeCollectionViewCell`.
  111. /// :nodoc:
  112. override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  113. guard let superview = superview else { return false }
  114. let point = convert(point, to: superview)
  115. if !UIAccessibility.isVoiceOverRunning {
  116. for cell in collectionView?.swipeCells ?? [] {
  117. if (cell.state == .left || cell.state == .right) && !cell.contains(point: point) {
  118. collectionView?.hideSwipeCell()
  119. return false
  120. }
  121. }
  122. }
  123. return contains(point: point)
  124. }
  125. func contains(point: CGPoint) -> Bool {
  126. return frame.contains(point)
  127. }
  128. // Override hitTest(_:with:) here so that we can make sure our `actionsView` gets the touch event
  129. // if it's supposed to, since otherwise, our `contentView` will swallow it and pass it up to
  130. // the collection view.
  131. /// :nodoc:
  132. open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  133. guard
  134. let actionsView = actionsView,
  135. isHidden == false
  136. else { return super.hitTest(point, with: event) }
  137. let modifiedPoint = actionsView.convert(point, from: self)
  138. return actionsView.hitTest(modifiedPoint, with: event) ?? super.hitTest(point, with: event)
  139. }
  140. /// :nodoc:
  141. override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  142. return swipeController.gestureRecognizerShouldBegin(gestureRecognizer)
  143. }
  144. /// :nodoc:
  145. open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  146. super.traitCollectionDidChange(previousTraitCollection)
  147. swipeController.traitCollectionDidChange(from: previousTraitCollection, to: self.traitCollection)
  148. }
  149. @objc func handleCollectionPan(gesture: UIPanGestureRecognizer) {
  150. if gesture.state == .began {
  151. hideSwipe(animated: true)
  152. }
  153. }
  154. func reset() {
  155. contentView.clipsToBounds = false
  156. swipeController.reset()
  157. collectionView?.setGestureEnabled(true)
  158. }
  159. func resetSelectedState() {
  160. if isPreviouslySelected {
  161. if let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) {
  162. collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
  163. }
  164. }
  165. isPreviouslySelected = false
  166. }
  167. }
  168. extension SwipeCollectionViewCell: SwipeControllerDelegate {
  169. func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool {
  170. return true
  171. }
  172. func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? {
  173. guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return nil }
  174. return delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: orientation)
  175. }
  176. func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions {
  177. guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return SwipeOptions() }
  178. return delegate?.collectionView(collectionView, editActionsOptionsForItemAt: indexPath, for: orientation) ?? SwipeOptions()
  179. }
  180. func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? {
  181. guard let collectionView = collectionView else { return nil }
  182. return delegate?.visibleRect(for: collectionView)
  183. }
  184. func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) {
  185. guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return }
  186. // Remove highlight and deselect any selected cells
  187. super.isHighlighted = false
  188. isPreviouslySelected = isSelected
  189. collectionView.deselectItem(at: indexPath, animated: false)
  190. delegate?.collectionView(collectionView, willBeginEditingItemAt: indexPath, for: orientation)
  191. }
  192. func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) {
  193. guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self), let actionsView = self.actionsView else { return }
  194. resetSelectedState()
  195. delegate?.collectionView(collectionView, didEndEditingItemAt: indexPath, for: actionsView.orientation)
  196. }
  197. func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) {
  198. collectionView?.deleteItems(at: [indexPath])
  199. }
  200. }