import Foundation public class Action: UIAccessibilityCustomAction { let icon: UIImage? let confirmationIcon: UIImage? public let type: ActionType public let indexPath: IndexPath public init(accessibilityTitle: String, icon: UIImage?, confirmationIcon: UIImage?, type: ActionType, indexPath: IndexPath, target: Any?, selector: Selector) { self.icon = icon self.confirmationIcon = confirmationIcon self.type = type self.indexPath = indexPath super.init(name: accessibilityTitle, target: target, selector: selector) } } @objc public protocol ActionDelegate: NSObjectProtocol { @objc func availableActions(at indexPath: IndexPath) -> [Action] @objc func didPerformAction(_ action: Action) -> Bool @objc func willPerformAction(_ action: Action) -> Bool @objc optional func didPerformBatchEditToolbarAction(_ action: BatchEditToolbarAction, completion: @escaping (Bool) -> Void) @objc optional var availableBatchEditToolbarActions: [BatchEditToolbarAction] { get } } public enum ActionType { case delete, save, unsave, share private struct Icon { static let delete = UIImage(named: "swipe-action-delete", in: Bundle.wmf, compatibleWith: nil) static let save = UIImage(named: "swipe-action-save", in: Bundle.wmf, compatibleWith: nil) static let unsave = UIImage(named: "swipe-action-unsave", in: Bundle.wmf, compatibleWith: nil) static let share = UIImage(named: "swipe-action-share", in: Bundle.wmf, compatibleWith: nil) } public func action(with target: Any?, indexPath: IndexPath) -> Action { switch self { case .delete: return Action(accessibilityTitle: CommonStrings.deleteActionTitle, icon: Icon.delete, confirmationIcon: nil, type: .delete, indexPath: indexPath, target: target, selector: #selector(ActionDelegate.willPerformAction(_:))) case .save: return Action(accessibilityTitle: CommonStrings.saveTitle, icon: Icon.save, confirmationIcon: Icon.unsave, type: .save, indexPath: indexPath, target: target, selector: #selector(ActionDelegate.willPerformAction(_:))) case .unsave: return Action(accessibilityTitle: CommonStrings.accessibilitySavedTitle, icon: Icon.unsave, confirmationIcon: Icon.save, type: .unsave, indexPath: indexPath, target: target, selector: #selector(ActionDelegate.willPerformAction(_:))) case .share: return Action(accessibilityTitle: CommonStrings.shareActionTitle, icon: Icon.share, confirmationIcon: nil, type: .share, indexPath: indexPath, target: target, selector: #selector(ActionDelegate.willPerformAction(_:))) } } } public class ActionsView: SizeThatFitsView, Themeable { fileprivate let minButtonWidth: CGFloat = 60 var maximumWidth: CGFloat = 0 var buttonWidth: CGFloat = 0 var buttons: [UIButton] = [] var needsSubviews = true public var theme = Theme.standard internal var actions: [Action] = [] { didSet { activatedIndex = NSNotFound needsSubviews = true } } fileprivate var activatedIndex = NSNotFound func expand(_ action: Action) { guard let index = actions.firstIndex(of: action) else { return } bringSubviewToFront(buttons[index]) activatedIndex = index setNeedsLayout() } public override var frame: CGRect { didSet { setNeedsLayout() } } public override var bounds: CGRect { didSet { setNeedsLayout() } } public override func sizeThatFits(_ size: CGSize, apply: Bool) -> CGSize { let superSize = super.sizeThatFits(size, apply: apply) if apply { if size.width > 0 && needsSubviews { createSubviews(for: actions) needsSubviews = false } let isRTL = effectiveUserInterfaceLayoutDirection == .rightToLeft if activatedIndex == NSNotFound { let numberOfButtons = CGFloat(subviews.count) let buttonDelta = min(size.width, maximumWidth) / numberOfButtons var x: CGFloat = isRTL ? size.width - buttonWidth : 0 for button in buttons { button.frame = CGRect(x: x, y: 0, width: buttonWidth, height: size.height) if isRTL { x -= buttonDelta } else { x += buttonDelta } } } else { var x: CGFloat = isRTL ? size.width : 0 - (buttonWidth * CGFloat(buttons.count - 1)) for button in buttons { button.clipsToBounds = true if button.tag == activatedIndex { button.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height)) } else { button.frame = CGRect(x: x, y: 0, width: buttonWidth, height: size.height) x += buttonWidth } } } } let width = superSize.width == UIView.noIntrinsicMetric ? maximumWidth : superSize.width let height = superSize.height == UIView.noIntrinsicMetric ? 50 : superSize.height return CGSize(width: width, height: height) } func createSubviews(for actions: [Action]) { for view in subviews { view.removeFromSuperview() } buttons = [] var maxButtonWidth: CGFloat = 0 for (index, action) in actions.enumerated() { let button = UIButton(type: .custom) button.setImage(action.icon, for: .normal) button.titleLabel?.numberOfLines = 1 button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 14) button.tag = index switch action.type { case .delete: button.backgroundColor = theme.colors.destructive case .share: button.backgroundColor = theme.colors.secondaryAction case .save: button.backgroundColor = theme.colors.link case .unsave: button.backgroundColor = theme.colors.link } button.imageView?.tintColor = .white button.addTarget(self, action: #selector(willPerformAction(_:)), for: .touchUpInside) maxButtonWidth = max(maxButtonWidth, button.intrinsicContentSize.width) insertSubview(button, at: 0) buttons.append(button) } backgroundColor = buttons.last?.backgroundColor buttonWidth = max(minButtonWidth, maxButtonWidth) maximumWidth = buttonWidth * CGFloat(subviews.count) setNeedsLayout() } public weak var delegate: ActionDelegate? private var activeSender: UIButton? @objc func willPerformAction(_ sender: UIButton) { activeSender = sender let action = actions[sender.tag] _ = delegate?.willPerformAction(action) } func updateConfirmationImage(for action: Action, completion: @escaping () -> Bool) -> Bool { if let image = action.confirmationIcon { activeSender?.setImage(image, for: .normal) DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { _ = completion() } } else { return completion() } return true } public func apply(theme: Theme) { self.theme = theme backgroundColor = theme.colors.baseBackground } }