// // ActionToolBarContainer.swift // Mastodon // // Created by sxiaojian on 2021/2/1. // import os.log import UIKit import MastodonAsset import MastodonLocalization public protocol ActionToolbarContainerDelegate: AnyObject { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) } 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.Communication.share.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 var style: Style? 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.preservesSuperviewLayoutMargins = true container.isLayoutMarginsRelativeArrangement = true 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) } public func configure(for style: Style) { guard needsConfigure(for: style) else { return } self.style = style container.arrangedSubviews.forEach { subview in container.removeArrangedSubview(subview) subview.removeFromSuperview() } let buttons = [replyButton, reblogButton, favoriteButton, shareButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) 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: style.buttonTitleImagePadding) } // 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 switch style { case .inline: 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 = .fill replyButton.translatesAutoresizingMaskIntoConstraints = false reblogButton.translatesAutoresizingMaskIntoConstraints = false favoriteButton.translatesAutoresizingMaskIntoConstraints = false shareButton.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) container.addArrangedSubview(shareButton) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 44).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), ]) shareButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) shareButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) case .plain: buttons.forEach { button in button.contentHorizontalAlignment = .center } replyButton.setImage(ActionToolbarContainer.replyImage, for: .normal) reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal) favoriteButton.setImage(ActionToolbarContainer.starImage, for: .normal) container.axis = .horizontal container.spacing = 8 container.distribution = .fillEqually container.addArrangedSubview(replyButton) container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) } } private func needsConfigure(for style: Style) -> Bool { guard let oldStyle = self.style else { return true } return oldStyle != style } } extension ActionToolbarContainer { public enum Action: String, CaseIterable { case reply case reblog case like case share } public enum Style { case inline case plain var buttonTitleImagePadding: CGFloat { switch self { case .inline: return 4.0 case .plain: return 0 } } } 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 = "\(count) reply" // TODO: i18n } public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) reblogButton.setTitle(title, for: .normal) 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) } else { reblogButton.accessibilityTraits.remove(.selected) } reblogButton.accessibilityLabel = L10n.Plural.Count.reblog(count) } public func configureFavorite(count: Int, isEnabled: Bool, isHighlighted: Bool) { let title = ActionToolbarContainer.title(from: count) favoriteButton.setTitle(title, for: .normal) 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) } else { favoriteButton.accessibilityTraits.remove(.selected) } favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count) } } extension ActionToolbarContainer { private static func title(from number: Int?) -> String { guard let number = number, number > 0 else { return "" } return String(number) } } 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) { let toolbar = ActionToolbarContainer() toolbar.configure(for: .inline) return toolbar } .previewLayout(.fixed(width: 300, height: 44)) .previewDisplayName("Inline") } } } #endif