metatext/Views/UIKit/PollView.swift

252 lines
9.9 KiB
Swift
Raw Normal View History

2020-10-25 02:31:44 +00:00
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
2021-01-19 00:46:38 +00:00
import Mastodon
2020-10-25 02:31:44 +00:00
import UIKit
import ViewModels
final class PollView: UIView {
private let stackView = UIStackView()
private let bottomStackView = UIStackView()
2021-01-31 06:47:49 +00:00
private let voteButton = CapsuleButton()
2020-10-25 02:31:44 +00:00
private let refreshButton = UIButton(type: .system)
private let refreshDividerLabel = UILabel()
private let votesCountLabel = UILabel()
private let votesCountDividerLabel = UILabel()
private let expiryLabel = UILabel()
private var selectionCancellable: AnyCancellable?
var viewModel: StatusViewModel? {
didSet {
for view in stackView.arrangedSubviews {
stackView.removeArrangedSubview(view)
view.removeFromSuperview()
}
guard let viewModel = viewModel else {
selectionCancellable = nil
return
}
2021-02-02 18:02:30 +00:00
let accessibilityAttributedLabel = NSMutableAttributedString(
string: NSLocalizedString("status.poll.accessibility-label", comment: ""))
2020-10-25 02:31:44 +00:00
if !viewModel.isPollExpired, !viewModel.hasVotedInPoll {
for (index, option) in viewModel.pollOptions.enumerated() {
let button = PollOptionButton(
title: option.title,
2021-01-12 07:33:35 +00:00
emojis: viewModel.pollEmojis,
2021-02-22 07:10:34 +00:00
multipleSelection: viewModel.isPollMultipleSelection,
identityContext: viewModel.identityContext)
2020-10-25 02:31:44 +00:00
2021-02-22 07:10:34 +00:00
button.button.addAction(
2020-10-25 02:31:44 +00:00
UIAction { _ in
if viewModel.pollOptionSelections.contains(index) {
viewModel.pollOptionSelections.remove(index)
} else if viewModel.isPollMultipleSelection {
viewModel.pollOptionSelections.insert(index)
} else {
viewModel.pollOptionSelections = [index]
}
},
for: .touchUpInside)
stackView.addArrangedSubview(button)
}
} else {
for (index, option) in viewModel.pollOptions.enumerated() {
let resultView = PollResultView(
option: option,
2021-01-12 07:33:35 +00:00
emojis: viewModel.pollEmojis,
2020-10-25 02:31:44 +00:00
selected: viewModel.pollOwnVotes.contains(index),
multipleSelection: viewModel.isPollMultipleSelection,
2021-02-22 07:10:34 +00:00
votersCount: viewModel.pollVotersCount,
identityContext: viewModel.identityContext)
2020-10-25 02:31:44 +00:00
stackView.addArrangedSubview(resultView)
}
}
2021-02-02 18:02:30 +00:00
for (index, view) in stackView.arrangedSubviews.enumerated() {
var title: NSAttributedString?
var percent: String?
let indexLabel = String.localizedStringWithFormat(
NSLocalizedString("status.poll.option-%ld", comment: ""),
index + 1)
if let optionView = view as? PollOptionButton,
2021-02-22 07:10:34 +00:00
let attributedTitle = optionView.button.accessibilityAttributedLabel {
2021-02-02 18:02:30 +00:00
title = attributedTitle
let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel)
if viewModel.isPollMultipleSelection {
optionAccessibilityAttributedLabel.appendWithSeparator(
NSLocalizedString("compose.poll.accessibility.multiple-choices-allowed", comment: ""))
}
optionAccessibilityAttributedLabel.appendWithSeparator(attributedTitle)
optionView.accessibilityAttributedLabel = optionAccessibilityAttributedLabel
} else if let resultView = view as? PollResultView {
title = resultView.titleLabel.attributedText
percent = resultView.percentLabel.text
}
guard let presentTitle = title else { continue }
accessibilityAttributedLabel.appendWithSeparator(indexLabel)
accessibilityAttributedLabel.appendWithSeparator(presentTitle)
if let percent = percent {
accessibilityAttributedLabel.appendWithSeparator(percent)
}
}
2020-10-25 02:31:44 +00:00
if !viewModel.isPollExpired, !viewModel.hasVotedInPoll {
2021-01-31 06:47:49 +00:00
stackView.addArrangedSubview(voteButton)
2020-10-25 02:31:44 +00:00
selectionCancellable = viewModel.$pollOptionSelections.sink { [weak self] in
guard let self = self else { return }
for (index, view) in self.stackView.arrangedSubviews.enumerated() {
2021-02-22 07:10:34 +00:00
(view as? PollOptionButton)?.isSelected = $0.contains(index)
2020-10-25 02:31:44 +00:00
}
self.voteButton.isEnabled = !$0.isEmpty
}
} else {
selectionCancellable = nil
}
stackView.addArrangedSubview(bottomStackView)
2021-02-02 18:02:30 +00:00
let votesCount = String.localizedStringWithFormat(
2021-03-24 01:39:04 +00:00
NSLocalizedString("status.poll.participation-count-%ld", comment: ""),
2020-10-25 02:31:44 +00:00
viewModel.pollVotersCount)
2021-02-02 18:02:30 +00:00
votesCountLabel.text = votesCount
votesCountLabel.isAccessibilityElement = true
votesCountLabel.accessibilityLabel = votesCountLabel.text
accessibilityAttributedLabel.appendWithSeparator(votesCount)
2020-10-25 02:31:44 +00:00
if !viewModel.isPollExpired, let pollTimeLeft = viewModel.pollTimeLeft {
expiryLabel.text = String.localizedStringWithFormat(
2021-03-24 01:39:04 +00:00
NSLocalizedString("status.poll.time-left-%@", comment: ""),
2020-10-25 02:31:44 +00:00
pollTimeLeft)
refreshButton.isHidden = false
2021-02-02 18:02:30 +00:00
accessibilityCustomActions =
[UIAccessibilityCustomAction(
name: NSLocalizedString("status.poll.refresh", comment: "")) { [weak self] _ in
self?.viewModel?.refreshPoll()
return true
}]
2020-10-25 02:31:44 +00:00
} else {
expiryLabel.text = NSLocalizedString("status.poll.closed", comment: "")
refreshButton.isHidden = true
2021-02-02 18:02:30 +00:00
accessibilityCustomActions = nil
}
expiryLabel.isAccessibilityElement = true
expiryLabel.accessibilityLabel = expiryLabel.text
if let expiry = expiryLabel.text {
accessibilityAttributedLabel.appendWithSeparator(expiry)
2020-10-25 02:31:44 +00:00
}
refreshDividerLabel.isHidden = refreshButton.isHidden
2021-02-02 18:02:30 +00:00
self.accessibilityAttributedLabel = accessibilityAttributedLabel
2020-10-25 02:31:44 +00:00
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initialSetup()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension PollView {
2021-01-19 00:46:38 +00:00
static func estimatedHeight(width: CGFloat,
2021-01-26 00:06:35 +00:00
identityContext: IdentityContext,
2021-01-19 00:46:38 +00:00
status: Status,
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
if let poll = status.displayStatus.poll {
var height: CGFloat = 0
let open = !poll.expired && !poll.voted
for option in poll.options {
2022-11-08 03:46:03 +00:00
if open {
height += PollOptionButton.estimatedHeight(width: width, title: option.title)
} else {
height += PollResultView.estimatedHeight(width: width, title: option.title)
}
2021-01-19 00:46:38 +00:00
height += .defaultSpacing
}
if open {
height += .minimumButtonDimension + .defaultSpacing
}
height += .minimumButtonDimension / 2
return height
} else {
return 0
}
}
}
private extension PollView {
2020-10-25 02:31:44 +00:00
func initialSetup() {
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = .defaultSpacing
voteButton.setTitle(NSLocalizedString("status.poll.vote", comment: ""), for: .normal)
voteButton.addAction(UIAction { [weak self] _ in self?.viewModel?.vote() }, for: .touchUpInside)
bottomStackView.spacing = .compactSpacing
bottomStackView.addArrangedSubview(refreshButton)
refreshButton.titleLabel?.font = .preferredFont(forTextStyle: .caption1)
refreshButton.titleLabel?.adjustsFontForContentSizeCategory = true
refreshButton.setTitle(NSLocalizedString("status.poll.refresh", comment: ""), for: .normal)
refreshButton.addAction(UIAction { [weak self] _ in self?.viewModel?.refreshPoll() }, for: .touchUpInside)
for label in [refreshDividerLabel, votesCountLabel, votesCountDividerLabel, expiryLabel] {
bottomStackView.addArrangedSubview(label)
label.font = .preferredFont(forTextStyle: .caption1)
label.textColor = .secondaryLabel
label.adjustsFontForContentSizeCategory = true
}
refreshDividerLabel.text = ""
votesCountDividerLabel.text = ""
bottomStackView.addArrangedSubview(UIView())
2021-01-19 00:46:38 +00:00
let refreshButtonHeightConstraint = refreshButton.heightAnchor.constraint(
equalToConstant: .minimumButtonDimension / 2)
refreshButtonHeightConstraint.priority = .justBelowMax
2020-10-25 02:31:44 +00:00
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
2021-01-19 00:46:38 +00:00
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
refreshButtonHeightConstraint
2020-10-25 02:31:44 +00:00
])
}
}