SwipeActionsView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. //
  2. // SwipeActionsView.swift
  3. //
  4. // Created by Jeremy Koch
  5. // Copyright © 2017 Jeremy Koch. All rights reserved.
  6. //
  7. import UIKit
  8. protocol SwipeActionsViewDelegate: class {
  9. func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction)
  10. }
  11. class SwipeActionsView: UIView {
  12. weak var delegate: SwipeActionsViewDelegate?
  13. let transitionLayout: SwipeTransitionLayout
  14. var layoutContext: ActionsViewLayoutContext
  15. var feedbackGenerator: SwipeFeedback
  16. var expansionAnimator: SwipeAnimator?
  17. var expansionDelegate: SwipeExpanding? {
  18. return options.expansionDelegate ?? (expandableAction?.hasBackgroundColor == false ? ScaleAndAlphaExpansion.default : nil)
  19. }
  20. weak var safeAreaInsetView: UIView?
  21. let orientation: SwipeActionsOrientation
  22. let actions: [SwipeAction]
  23. let options: SwipeOptions
  24. var buttons: [SwipeActionButton] = []
  25. var minimumButtonWidth: CGFloat = 0
  26. var maximumImageHeight: CGFloat {
  27. return actions.reduce(0, { initial, next in max(initial, next.image?.size.height ?? 0) })
  28. }
  29. var safeAreaMargin: CGFloat {
  30. guard #available(iOS 11, *) else { return 0 }
  31. guard let scrollView = self.safeAreaInsetView else { return 0 }
  32. return orientation == .left ? scrollView.safeAreaInsets.left : scrollView.safeAreaInsets.right
  33. }
  34. var visibleWidth: CGFloat = 0 {
  35. didSet {
  36. // If necessary, adjust for safe areas
  37. visibleWidth = max(0, visibleWidth - safeAreaMargin)
  38. let preLayoutVisibleWidths = transitionLayout.visibleWidthsForViews(with: layoutContext)
  39. layoutContext = ActionsViewLayoutContext.newContext(for: self)
  40. transitionLayout.container(view: self, didChangeVisibleWidthWithContext: layoutContext)
  41. setNeedsLayout()
  42. layoutIfNeeded()
  43. notifyVisibleWidthChanged(oldWidths: preLayoutVisibleWidths,
  44. newWidths: transitionLayout.visibleWidthsForViews(with: layoutContext))
  45. }
  46. }
  47. var preferredWidth: CGFloat {
  48. return minimumButtonWidth * CGFloat(actions.count) + safeAreaMargin
  49. }
  50. var contentSize: CGSize {
  51. if options.expansionStyle?.elasticOverscroll != true || visibleWidth < preferredWidth {
  52. return CGSize(width: visibleWidth, height: bounds.height)
  53. } else {
  54. let scrollRatio = max(0, visibleWidth - preferredWidth)
  55. return CGSize(width: preferredWidth + (scrollRatio * 0.25), height: bounds.height)
  56. }
  57. }
  58. private(set) var expanded: Bool = false
  59. var expandableAction: SwipeAction? {
  60. return options.expansionStyle != nil ? actions.last : nil
  61. }
  62. init(contentEdgeInsets: UIEdgeInsets,
  63. maxSize: CGSize,
  64. safeAreaInsetView: UIView,
  65. options: SwipeOptions,
  66. orientation: SwipeActionsOrientation,
  67. actions: [SwipeAction]) {
  68. self.safeAreaInsetView = safeAreaInsetView
  69. self.options = options
  70. self.orientation = orientation
  71. self.actions = actions.reversed()
  72. switch options.transitionStyle {
  73. case .border:
  74. transitionLayout = BorderTransitionLayout()
  75. case .reveal:
  76. transitionLayout = RevealTransitionLayout()
  77. default:
  78. transitionLayout = DragTransitionLayout()
  79. }
  80. self.layoutContext = ActionsViewLayoutContext(numberOfActions: actions.count, orientation: orientation)
  81. feedbackGenerator = SwipeFeedback(style: .light)
  82. feedbackGenerator.prepare()
  83. super.init(frame: .zero)
  84. clipsToBounds = true
  85. translatesAutoresizingMaskIntoConstraints = false
  86. backgroundColor = options.backgroundColor ?? #colorLiteral(red: 0.862745098, green: 0.862745098, blue: 0.862745098, alpha: 1)
  87. buttons = addButtons(for: self.actions, withMaximum: maxSize, contentEdgeInsets: contentEdgeInsets)
  88. }
  89. required init?(coder aDecoder: NSCoder) {
  90. fatalError("init(coder:) has not been implemented")
  91. }
  92. func addButtons(for actions: [SwipeAction], withMaximum size: CGSize, contentEdgeInsets: UIEdgeInsets) -> [SwipeActionButton] {
  93. let buttons: [SwipeActionButton] = actions.map({ action in
  94. let actionButton = SwipeActionButton(action: action)
  95. actionButton.addTarget(self, action: #selector(actionTapped(button:)), for: .touchUpInside)
  96. actionButton.autoresizingMask = [.flexibleHeight, orientation == .right ? .flexibleRightMargin : .flexibleLeftMargin]
  97. actionButton.spacing = options.buttonSpacing ?? 8
  98. actionButton.contentEdgeInsets = buttonEdgeInsets(fromOptions: options)
  99. return actionButton
  100. })
  101. let maximum = options.maximumButtonWidth ?? (size.width - 30) / CGFloat(actions.count)
  102. let minimum = options.minimumButtonWidth ?? min(maximum, 74)
  103. minimumButtonWidth = buttons.reduce(minimum, { initial, next in max(initial, next.preferredWidth(maximum: maximum)) })
  104. buttons.enumerated().forEach { (index, button) in
  105. let action = actions[index]
  106. let frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height))
  107. let wrapperView = SwipeActionButtonWrapperView(frame: frame, action: action, orientation: orientation, contentWidth: minimumButtonWidth)
  108. wrapperView.translatesAutoresizingMaskIntoConstraints = false
  109. wrapperView.addSubview(button)
  110. if let effect = action.backgroundEffect {
  111. let effectView = UIVisualEffectView(effect: effect)
  112. effectView.frame = wrapperView.frame
  113. effectView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  114. effectView.contentView.addSubview(wrapperView)
  115. addSubview(effectView)
  116. } else {
  117. addSubview(wrapperView)
  118. }
  119. button.frame = wrapperView.contentRect
  120. button.maximumImageHeight = maximumImageHeight
  121. button.verticalAlignment = options.buttonVerticalAlignment
  122. button.shouldHighlight = action.hasBackgroundColor
  123. wrapperView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
  124. wrapperView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
  125. let topConstraint = wrapperView.topAnchor.constraint(equalTo: topAnchor, constant: contentEdgeInsets.top)
  126. topConstraint.priority = contentEdgeInsets.top == 0 ? .required : .defaultHigh
  127. topConstraint.isActive = true
  128. let bottomConstraint = wrapperView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1 * contentEdgeInsets.bottom)
  129. bottomConstraint.priority = contentEdgeInsets.bottom == 0 ? .required : .defaultHigh
  130. bottomConstraint.isActive = true
  131. if contentEdgeInsets != .zero {
  132. let heightConstraint = wrapperView.heightAnchor.constraint(greaterThanOrEqualToConstant: button.intrinsicContentSize.height)
  133. heightConstraint.priority = .required
  134. heightConstraint.isActive = true
  135. }
  136. }
  137. return buttons
  138. }
  139. @objc func actionTapped(button: SwipeActionButton) {
  140. guard let index = buttons.index(of: button) else { return }
  141. delegate?.swipeActionsView(self, didSelect: actions[index])
  142. }
  143. func buttonEdgeInsets(fromOptions options: SwipeOptions) -> UIEdgeInsets {
  144. let padding = options.buttonPadding ?? 8
  145. return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
  146. }
  147. func setExpanded(expanded: Bool, feedback: Bool = false) {
  148. guard self.expanded != expanded else { return }
  149. self.expanded = expanded
  150. if feedback {
  151. feedbackGenerator.impactOccurred()
  152. feedbackGenerator.prepare()
  153. }
  154. let timingParameters = expansionDelegate?.animationTimingParameters(buttons: buttons.reversed(), expanding: expanded)
  155. if expansionAnimator?.isRunning == true {
  156. expansionAnimator?.stopAnimation(true)
  157. }
  158. if #available(iOS 10, *) {
  159. expansionAnimator = UIViewPropertyAnimator(duration: timingParameters?.duration ?? 0.6, dampingRatio: 1.0)
  160. } else {
  161. expansionAnimator = UIViewSpringAnimator(duration: timingParameters?.duration ?? 0.6,
  162. damping: 1.0,
  163. initialVelocity: 1.0)
  164. }
  165. expansionAnimator?.addAnimations {
  166. self.setNeedsLayout()
  167. self.layoutIfNeeded()
  168. }
  169. expansionAnimator?.startAnimation(afterDelay: timingParameters?.delay ?? 0)
  170. notifyExpansion(expanded: expanded)
  171. }
  172. func notifyVisibleWidthChanged(oldWidths: [CGFloat], newWidths: [CGFloat]) {
  173. DispatchQueue.main.async {
  174. oldWidths.enumerated().forEach { index, oldWidth in
  175. let newWidth = newWidths[index]
  176. if oldWidth != newWidth {
  177. let context = SwipeActionTransitioningContext(actionIdentifier: self.actions[index].identifier,
  178. button: self.buttons[index],
  179. newPercentVisible: newWidth / self.minimumButtonWidth,
  180. oldPercentVisible: oldWidth / self.minimumButtonWidth,
  181. wrapperView: self.subviews[index])
  182. self.actions[index].transitionDelegate?.didTransition(with: context)
  183. }
  184. }
  185. }
  186. }
  187. func notifyExpansion(expanded: Bool) {
  188. guard let expandedButton = buttons.last else { return }
  189. expansionDelegate?.actionButton(expandedButton, didChange: expanded, otherActionButtons: buttons.dropLast().reversed())
  190. }
  191. func createDeletionMask() -> UIView {
  192. let mask = UIView(frame: CGRect(x: min(0, frame.minX), y: 0, width: bounds.width * 2, height: bounds.height))
  193. mask.backgroundColor = UIColor.white
  194. return mask
  195. }
  196. override func layoutSubviews() {
  197. super.layoutSubviews()
  198. for subview in subviews.enumerated() {
  199. transitionLayout.layout(view: subview.element, atIndex: subview.offset, with: layoutContext)
  200. }
  201. if expanded {
  202. subviews.last?.frame.origin.x = 0 + bounds.origin.x
  203. }
  204. }
  205. }
  206. class SwipeActionButtonWrapperView: UIView {
  207. let contentRect: CGRect
  208. var actionBackgroundColor: UIColor?
  209. init(frame: CGRect, action: SwipeAction, orientation: SwipeActionsOrientation, contentWidth: CGFloat) {
  210. switch orientation {
  211. case .left:
  212. contentRect = CGRect(x: frame.width - contentWidth, y: 0, width: contentWidth, height: frame.height)
  213. case .right:
  214. contentRect = CGRect(x: 0, y: 0, width: contentWidth, height: frame.height)
  215. }
  216. super.init(frame: frame)
  217. configureBackgroundColor(with: action)
  218. }
  219. override func draw(_ rect: CGRect) {
  220. super.draw(rect)
  221. if let actionBackgroundColor = self.actionBackgroundColor, let context = UIGraphicsGetCurrentContext() {
  222. actionBackgroundColor.setFill()
  223. context.fill(rect);
  224. }
  225. }
  226. func configureBackgroundColor(with action: SwipeAction) {
  227. guard action.hasBackgroundColor else {
  228. isOpaque = false
  229. return
  230. }
  231. if let backgroundColor = action.backgroundColor {
  232. actionBackgroundColor = backgroundColor
  233. } else {
  234. switch action.style {
  235. case .destructive:
  236. actionBackgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
  237. default:
  238. actionBackgroundColor = #colorLiteral(red: 0.862745098, green: 0.862745098, blue: 0.862745098, alpha: 1)
  239. }
  240. }
  241. }
  242. required init?(coder aDecoder: NSCoder) {
  243. fatalError("init(coder:) has not been implemented")
  244. }
  245. }