mastodon-ios/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift

314 lines
13 KiB
Swift
Raw Normal View History

//
// 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)
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")
2022-02-11 08:27:29 +01:00
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 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 {
2022-02-18 10:49:20 +01:00
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)
}
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
2022-02-14 12:34:22 +01:00
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: 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),
])
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 bookmark
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 = 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.reblog(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)
2022-02-14 12:34:22 +01:00
if isHighlighted {
reblogButton.accessibilityTraits.insert(.selected)
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unreblog
2022-02-14 12:34:22 +01:00
} else {
reblogButton.accessibilityTraits.remove(.selected)
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog
2022-02-14 12:34:22 +01:00
}
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)
2022-02-14 12:34:22 +01:00
if isHighlighted {
favoriteButton.accessibilityTraits.insert(.selected)
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unfavorite
2022-02-14 12:34:22 +01:00
} else {
favoriteButton.accessibilityTraits.remove(.selected)
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite
2022-02-14 12:34:22 +01:00
}
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 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