// // ActionToolBarContainer.swift // Mastodon // // Created by sxiaojian on 2021/2/1. // import os.log import UIKit import MastodonAsset import MastodonLocalization import MastodonExtension public protocol ActionToolbarContainerDelegate: AnyObject { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showReblogs action: UIAccessibilityCustomAction) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showFavorites action: UIAccessibilityCustomAction) } public final class ActionToolbarContainer: UIView { let logger = Logger(subsystem: "ActionToolbarContainer", category: "Control") static let replyImage = Asset.Communication.bubbleLeftAndBubbleRight.image.withRenderingMode(.alwaysTemplate) static let reblogImage = Asset.Arrow.repeat.image.withRenderingMode(.alwaysTemplate) static let starImage = Asset.ObjectsAndTools.star.image.withRenderingMode(.alwaysTemplate) static let starFillImage = Asset.ObjectsAndTools.starFill.image.withRenderingMode(.alwaysTemplate) static let shareImage = Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate) public let replyButton = HighlightDimmableButton() public let reblogButton = HighlightDimmableButton() public let favoriteButton = HighlightDimmableButton() public let shareButton = HighlightDimmableButton() public weak var delegate: ActionToolbarContainerDelegate? private let container = UIStackView() private let firstContainer = UIStackView() private let secondContainer = UIStackView() private var isAccessibilityCategory: Bool? private var shareButtonWidthConstraint: NSLayoutConstraint? public override init(frame: CGRect) { super.init(frame: frame) _init() } public required init?(coder: NSCoder) { super.init(coder: coder) _init() } } extension ActionToolbarContainer { private func _init() { container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) NSLayoutConstraint.activate([ container.topAnchor.constraint(equalTo: topAnchor), container.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: container.trailingAnchor), bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) replyButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) shareButton.addTarget(self, action: #selector(ActionToolbarContainer.buttonDidPressed(_:)), for: .touchUpInside) let buttons = [replyButton, reblogButton, favoriteButton, shareButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = UIFontMetrics(forTextStyle: .caption1) .scaledFont(for: .monospacedDigitSystemFont(ofSize: 12, weight: .regular)) button.titleLabel?.adjustsFontForContentSizeCategory = true button.setTitle("", for: .normal) button.setTitleColor(.secondaryLabel, for: .normal) button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) button.setInsets(forContentPadding: .zero, imageTitlePadding: 4) button.adjustsImageSizeForAccessibilityContentSizeCategory = true button.setContentCompressionResistancePriority(.defaultHigh + 100, for: .horizontal) } // add more expand for menu button shareButton.expandEdgeInsets = UIEdgeInsets(top: -10, left: -20, bottom: -10, right: -20) replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state shareButton.accessibilityLabel = L10n.Common.Controls.Actions.share buttons.forEach { button in button.contentHorizontalAlignment = .leading } replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) shareButton.setImage(ActionToolbarContainer.shareImage, for: .normal) container.axis = .horizontal container.distribution = .equalSpacing replyButton.translatesAutoresizingMaskIntoConstraints = false reblogButton.translatesAutoresizingMaskIntoConstraints = false favoriteButton.translatesAutoresizingMaskIntoConstraints = false shareButton.translatesAutoresizingMaskIntoConstraints = false firstContainer.translatesAutoresizingMaskIntoConstraints = false firstContainer.axis = .horizontal firstContainer.distribution = .equalSpacing secondContainer.translatesAutoresizingMaskIntoConstraints = false secondContainer.axis = .horizontal secondContainer.distribution = .equalSpacing shareButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) shareButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) shareButtonWidthConstraint = replyButton.widthAnchor.constraint(equalTo: shareButton.widthAnchor) traitCollectionDidChange(nil) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 36).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: reblogButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: shareButton.heightAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh), ]) } public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) let isAccessibilityCategory = traitCollection.preferredContentSizeCategory.isAccessibilityCategory guard isAccessibilityCategory != self.isAccessibilityCategory else { return } self.isAccessibilityCategory = isAccessibilityCategory if isAccessibilityCategory { container.axis = .vertical container.spacing = 12 firstContainer.addArrangedSubview(replyButton) firstContainer.addArrangedSubview(reblogButton) container.addArrangedSubview(firstContainer) secondContainer.addArrangedSubview(favoriteButton) secondContainer.addArrangedSubview(shareButton) container.addArrangedSubview(secondContainer) } else { container.axis = .horizontal container.spacing = 0 container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) container.addArrangedSubview(shareButton) firstContainer.removeFromSuperview() secondContainer.removeFromSuperview() } shareButtonWidthConstraint!.isActive = isAccessibilityCategory } } extension ActionToolbarContainer { public enum Action: String, CaseIterable { case reply case reblog case like case share } private func isReblogButtonHighlightStateDidChange(to isHighlight: Bool) { let tintColor = isHighlight ? Asset.Colors.successGreen.color : Asset.Colors.Button.actionToolbar.color reblogButton.tintColor = tintColor reblogButton.setTitleColor(tintColor, for: .normal) reblogButton.setTitleColor(tintColor, for: .highlighted) } private func isFavoriteButtonHighlightStateDidChange(to isHighlight: Bool) { let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Button.actionToolbar.color favoriteButton.tintColor = tintColor favoriteButton.setTitleColor(tintColor, for: .normal) favoriteButton.setTitleColor(tintColor, for: .highlighted) } } extension ActionToolbarContainer { @objc private func buttonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let _action: Action? switch sender { case replyButton: _action = .reply case reblogButton: _action = .reblog case favoriteButton: _action = .like case shareButton: _action = .share default: _action = nil } guard let action = _action else { assertionFailure() return } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(action.rawValue) button pressed") delegate?.actionToolbarContainer(self, buttonDidPressed: sender, action: action) } } extension ActionToolbarContainer { public func configureReply(count: Int, isEnabled: Bool) { let title = ActionToolbarContainer.title(from: count) replyButton.setTitle(title, for: .normal) replyButton.accessibilityLabel = L10n.Common.Controls.Actions.reply replyButton.accessibilityValue = L10n.Plural.Count.reply(count) } public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) reblogButton.setTitle(title, for: .normal) reblogButton.accessibilityValue = L10n.Plural.Count.reblogA11y(count) reblogButton.isEnabled = isEnabled reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) let tintColor = isHighlighted ? Asset.Colors.successGreen.color : Asset.Colors.Button.actionToolbar.color reblogButton.tintColor = tintColor reblogButton.setTitleColor(tintColor, for: .normal) reblogButton.setTitleColor(tintColor, for: .highlighted) if isHighlighted { reblogButton.accessibilityTraits.insert(.selected) reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.A11YLabels.unreblog } else { reblogButton.accessibilityTraits.remove(.selected) reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.A11YLabels.reblog } reblogButton.accessibilityCustomActions = [ UIAccessibilityCustomAction(name: "Show All Reblogs") { [weak self] action in guard let self = self else { return false } self.delegate?.actionToolbarContainer(self, showReblogs: action) return true } ] } public func configureFavorite(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) favoriteButton.setTitle(title, for: .normal) favoriteButton.accessibilityValue = L10n.Plural.Count.favorite(count) favoriteButton.isEnabled = isEnabled let image = isHighlighted ? ActionToolbarContainer.starFillImage : ActionToolbarContainer.starImage favoriteButton.setImage(image, for: .normal) let tintColor = isHighlighted ? Asset.Colors.systemOrange.color : Asset.Colors.Button.actionToolbar.color favoriteButton.tintColor = tintColor favoriteButton.setTitleColor(tintColor, for: .normal) favoriteButton.setTitleColor(tintColor, for: .highlighted) if isHighlighted { favoriteButton.accessibilityTraits.insert(.selected) favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unfavorite } else { favoriteButton.accessibilityTraits.remove(.selected) favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite } favoriteButton.accessibilityCustomActions = [ UIAccessibilityCustomAction(name: "Show All Favorites") { [weak self] action in guard let self = self else { return false } self.delegate?.actionToolbarContainer(self, showFavorites: action) return true } ] } } extension ActionToolbarContainer { private static func title(from number: Int?) -> String { guard let number = number, number > 0 else { return "" } return number.asAbbreviatedCountString() } } extension ActionToolbarContainer { public override var accessibilityElements: [Any]? { get { [replyButton, reblogButton, favoriteButton, shareButton] } set { } } } #if DEBUG import SwiftUI struct ActionToolbarContainer_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 300) { ActionToolbarContainer() } .previewLayout(.fixed(width: 300, height: 44)) .previewDisplayName("Inline") } } } #endif