123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- //
- // SwipeActionsView.swift
- //
- // Created by Jeremy Koch
- // Copyright © 2017 Jeremy Koch. All rights reserved.
- //
- import UIKit
- protocol SwipeActionsViewDelegate: class {
- func swipeActionsView(_ swipeActionsView: SwipeActionsView, didSelect action: SwipeAction)
- }
- class SwipeActionsView: UIView {
- weak var delegate: SwipeActionsViewDelegate?
-
- let transitionLayout: SwipeTransitionLayout
- var layoutContext: ActionsViewLayoutContext
-
- var feedbackGenerator: SwipeFeedback
-
- var expansionAnimator: SwipeAnimator?
-
- var expansionDelegate: SwipeExpanding? {
- return options.expansionDelegate ?? (expandableAction?.hasBackgroundColor == false ? ScaleAndAlphaExpansion.default : nil)
- }
- weak var safeAreaInsetView: UIView?
- let orientation: SwipeActionsOrientation
- let actions: [SwipeAction]
- let options: SwipeOptions
-
- var buttons: [SwipeActionButton] = []
-
- var minimumButtonWidth: CGFloat = 0
- var maximumImageHeight: CGFloat {
- return actions.reduce(0, { initial, next in max(initial, next.image?.size.height ?? 0) })
- }
-
- var safeAreaMargin: CGFloat {
- guard #available(iOS 11, *) else { return 0 }
- guard let scrollView = self.safeAreaInsetView else { return 0 }
- return orientation == .left ? scrollView.safeAreaInsets.left : scrollView.safeAreaInsets.right
- }
- var visibleWidth: CGFloat = 0 {
- didSet {
- // If necessary, adjust for safe areas
- visibleWidth = max(0, visibleWidth - safeAreaMargin)
- let preLayoutVisibleWidths = transitionLayout.visibleWidthsForViews(with: layoutContext)
- layoutContext = ActionsViewLayoutContext.newContext(for: self)
-
- transitionLayout.container(view: self, didChangeVisibleWidthWithContext: layoutContext)
-
- setNeedsLayout()
- layoutIfNeeded()
-
- notifyVisibleWidthChanged(oldWidths: preLayoutVisibleWidths,
- newWidths: transitionLayout.visibleWidthsForViews(with: layoutContext))
- }
- }
- var preferredWidth: CGFloat {
- return minimumButtonWidth * CGFloat(actions.count) + safeAreaMargin
- }
- var contentSize: CGSize {
- if options.expansionStyle?.elasticOverscroll != true || visibleWidth < preferredWidth {
- return CGSize(width: visibleWidth, height: bounds.height)
- } else {
- let scrollRatio = max(0, visibleWidth - preferredWidth)
- return CGSize(width: preferredWidth + (scrollRatio * 0.25), height: bounds.height)
- }
- }
-
- private(set) var expanded: Bool = false
-
- var expandableAction: SwipeAction? {
- return options.expansionStyle != nil ? actions.last : nil
- }
-
- init(contentEdgeInsets: UIEdgeInsets,
- maxSize: CGSize,
- safeAreaInsetView: UIView,
- options: SwipeOptions,
- orientation: SwipeActionsOrientation,
- actions: [SwipeAction]) {
-
- self.safeAreaInsetView = safeAreaInsetView
- self.options = options
- self.orientation = orientation
- self.actions = actions.reversed()
-
- switch options.transitionStyle {
- case .border:
- transitionLayout = BorderTransitionLayout()
- case .reveal:
- transitionLayout = RevealTransitionLayout()
- default:
- transitionLayout = DragTransitionLayout()
- }
-
- self.layoutContext = ActionsViewLayoutContext(numberOfActions: actions.count, orientation: orientation)
-
- feedbackGenerator = SwipeFeedback(style: .light)
- feedbackGenerator.prepare()
-
- super.init(frame: .zero)
-
- clipsToBounds = true
- translatesAutoresizingMaskIntoConstraints = false
-
- #if canImport(Combine)
- if let backgroundColor = options.backgroundColor {
- self.backgroundColor = backgroundColor
- }
- else if #available(iOS 13.0, *) {
- backgroundColor = UIColor.systemGray5
- } else {
- backgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
- }
- #else
- if let backgroundColor = options.backgroundColor {
- self.backgroundColor = backgroundColor
- }
- else {
- backgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
- }
- #endif
-
- buttons = addButtons(for: self.actions, withMaximum: maxSize, contentEdgeInsets: contentEdgeInsets)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- func addButtons(for actions: [SwipeAction], withMaximum size: CGSize, contentEdgeInsets: UIEdgeInsets) -> [SwipeActionButton] {
- let buttons: [SwipeActionButton] = actions.map({ action in
- let actionButton = SwipeActionButton(action: action)
- actionButton.addTarget(self, action: #selector(actionTapped(button:)), for: .touchUpInside)
- actionButton.autoresizingMask = [.flexibleHeight, orientation == .right ? .flexibleRightMargin : .flexibleLeftMargin]
- actionButton.spacing = options.buttonSpacing ?? 8
- actionButton.contentEdgeInsets = buttonEdgeInsets(fromOptions: options)
- return actionButton
- })
-
- let maximum = options.maximumButtonWidth ?? (size.width - 30) / CGFloat(actions.count)
- let minimum = options.minimumButtonWidth ?? min(maximum, 74)
- minimumButtonWidth = buttons.reduce(minimum, { initial, next in max(initial, next.preferredWidth(maximum: maximum)) })
-
-
- buttons.enumerated().forEach { (index, button) in
- let action = actions[index]
- let frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height))
- let wrapperView = SwipeActionButtonWrapperView(frame: frame, action: action, orientation: orientation, contentWidth: minimumButtonWidth)
- wrapperView.translatesAutoresizingMaskIntoConstraints = false
- wrapperView.addSubview(button)
-
- if let effect = action.backgroundEffect {
- let effectView = UIVisualEffectView(effect: effect)
- effectView.frame = wrapperView.frame
- effectView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- effectView.contentView.addSubview(wrapperView)
- addSubview(effectView)
- } else {
- addSubview(wrapperView)
- }
-
- button.frame = wrapperView.contentRect
- button.maximumImageHeight = maximumImageHeight
- button.verticalAlignment = options.buttonVerticalAlignment
- button.shouldHighlight = action.hasBackgroundColor
-
- wrapperView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
- wrapperView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
-
- let topConstraint = wrapperView.topAnchor.constraint(equalTo: topAnchor, constant: contentEdgeInsets.top)
- topConstraint.priority = contentEdgeInsets.top == 0 ? .required : .defaultHigh
- topConstraint.isActive = true
-
- let bottomConstraint = wrapperView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -1 * contentEdgeInsets.bottom)
- bottomConstraint.priority = contentEdgeInsets.bottom == 0 ? .required : .defaultHigh
- bottomConstraint.isActive = true
-
- if contentEdgeInsets != .zero {
- let heightConstraint = wrapperView.heightAnchor.constraint(greaterThanOrEqualToConstant: button.intrinsicContentSize.height)
- heightConstraint.priority = .required
- heightConstraint.isActive = true
- }
- }
- return buttons
- }
-
- @objc func actionTapped(button: SwipeActionButton) {
- guard let index = buttons.firstIndex(of: button) else { return }
- delegate?.swipeActionsView(self, didSelect: actions[index])
- }
-
- func buttonEdgeInsets(fromOptions options: SwipeOptions) -> UIEdgeInsets {
- let padding = options.buttonPadding ?? 8
- return UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
- }
-
- func setExpanded(expanded: Bool, feedback: Bool = false) {
- guard self.expanded != expanded else { return }
-
- self.expanded = expanded
-
- if feedback {
- feedbackGenerator.impactOccurred()
- feedbackGenerator.prepare()
- }
-
- let timingParameters = expansionDelegate?.animationTimingParameters(buttons: buttons.reversed(), expanding: expanded)
-
- if expansionAnimator?.isRunning == true {
- expansionAnimator?.stopAnimation(true)
- }
-
- if #available(iOS 10, *) {
- expansionAnimator = UIViewPropertyAnimator(duration: timingParameters?.duration ?? 0.6, dampingRatio: 1.0)
- } else {
- expansionAnimator = UIViewSpringAnimator(duration: timingParameters?.duration ?? 0.6,
- damping: 1.0,
- initialVelocity: 1.0)
- }
-
- expansionAnimator?.addAnimations {
- self.setNeedsLayout()
- self.layoutIfNeeded()
- }
-
- expansionAnimator?.startAnimation(afterDelay: timingParameters?.delay ?? 0)
-
- notifyExpansion(expanded: expanded)
- }
-
- func notifyVisibleWidthChanged(oldWidths: [CGFloat], newWidths: [CGFloat]) {
- DispatchQueue.main.async {
- oldWidths.enumerated().forEach { index, oldWidth in
- let newWidth = newWidths[index]
- if oldWidth != newWidth {
- let context = SwipeActionTransitioningContext(actionIdentifier: self.actions[index].identifier,
- button: self.buttons[index],
- newPercentVisible: newWidth / self.minimumButtonWidth,
- oldPercentVisible: oldWidth / self.minimumButtonWidth,
- wrapperView: self.subviews[index])
-
- self.actions[index].transitionDelegate?.didTransition(with: context)
- }
- }
- }
- }
-
- func notifyExpansion(expanded: Bool) {
- guard let expandedButton = buttons.last else { return }
- expansionDelegate?.actionButton(expandedButton, didChange: expanded, otherActionButtons: buttons.dropLast().reversed())
- }
-
- func createDeletionMask() -> UIView {
- let mask = UIView(frame: CGRect(x: min(0, frame.minX), y: 0, width: bounds.width * 2, height: bounds.height))
- mask.backgroundColor = UIColor.white
- return mask
- }
-
- override func layoutSubviews() {
- super.layoutSubviews()
-
- for subview in subviews.enumerated() {
- transitionLayout.layout(view: subview.element, atIndex: subview.offset, with: layoutContext)
- }
-
- if expanded {
- subviews.last?.frame.origin.x = 0 + bounds.origin.x
- }
- }
- }
- class SwipeActionButtonWrapperView: UIView {
- let contentRect: CGRect
- var actionBackgroundColor: UIColor?
-
- init(frame: CGRect, action: SwipeAction, orientation: SwipeActionsOrientation, contentWidth: CGFloat) {
- switch orientation {
- case .left:
- contentRect = CGRect(x: frame.width - contentWidth, y: 0, width: contentWidth, height: frame.height)
- case .right:
- contentRect = CGRect(x: 0, y: 0, width: contentWidth, height: frame.height)
- }
-
- super.init(frame: frame)
-
- configureBackgroundColor(with: action)
- }
-
- override func draw(_ rect: CGRect) {
- super.draw(rect)
-
- if let actionBackgroundColor = self.actionBackgroundColor, let context = UIGraphicsGetCurrentContext() {
- actionBackgroundColor.setFill()
- context.fill(rect);
- }
- }
-
- func configureBackgroundColor(with action: SwipeAction) {
- guard action.hasBackgroundColor else {
- isOpaque = false
- return
- }
-
- if let backgroundColor = action.backgroundColor {
- actionBackgroundColor = backgroundColor
- } else {
- switch action.style {
- case .destructive:
- #if canImport(Combine)
- if #available(iOS 13.0, *) {
- actionBackgroundColor = UIColor.systemRed
- } else {
- actionBackgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
- }
- #else
- actionBackgroundColor = #colorLiteral(red: 1, green: 0.2352941176, blue: 0.1882352941, alpha: 1)
- #endif
- default:
- #if canImport(Combine)
- if #available(iOS 13.0, *) {
- actionBackgroundColor = UIColor.systemGray3
- } else {
- actionBackgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
- }
- #else
- actionBackgroundColor = #colorLiteral(red: 0.7803494334, green: 0.7761332393, blue: 0.7967314124, alpha: 1)
- #endif
- }
- }
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
|