metatext/Views/UIKit/AccountFieldView.swift
Justin Mazzocchi 8b2acf1ace
Animated emoji
2021-02-21 23:10:34 -08:00

223 lines
9.3 KiB
Swift

// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import Mastodon
import UIKit
import ViewModels
final class AccountFieldView: UIView {
let nameLabel = AnimatedAttachmentLabel()
let valueTextView = TouchFallthroughTextView()
let checkButton = UIButton()
private var valueTextViewTrailingConstraint: NSLayoutConstraint?
private var cancellables = Set<AnyCancellable>()
// swiftlint:disable:next function_body_length
init(name: String,
value: NSAttributedString,
verifiedAt: Date?,
emojis: [Emoji],
identityContext: IdentityContext) {
super.init(frame: .zero)
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
.sink { [weak self] _ in self?.configureUserInteractionEnabledForAccessibility() }
.store(in: &cancellables)
backgroundColor = .systemBackground
let nameBackgroundView = UIView()
addSubview(nameBackgroundView)
nameBackgroundView.translatesAutoresizingMaskIntoConstraints = false
nameBackgroundView.backgroundColor = .secondarySystemBackground
let valueBackgroundView = UIView()
addSubview(valueBackgroundView)
valueBackgroundView.translatesAutoresizingMaskIntoConstraints = false
valueBackgroundView.backgroundColor = verifiedAt == nil
? .systemBackground
: UIColor.systemGreen.withAlphaComponent(0.25)
addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.numberOfLines = 0
nameLabel.font = .preferredFont(forTextStyle: .headline)
nameLabel.textAlignment = .center
nameLabel.textColor = .secondaryLabel
let mutableName = NSMutableAttributedString(string: name)
mutableName.insert(emojis: emojis, view: nameLabel, identityContext: identityContext)
mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight)
nameLabel.attributedText = mutableName
let dividerView = UIView()
addSubview(dividerView)
dividerView.translatesAutoresizingMaskIntoConstraints = false
dividerView.backgroundColor = .separator
addSubview(valueTextView)
valueTextView.translatesAutoresizingMaskIntoConstraints = false
valueTextView.backgroundColor = .clear
if verifiedAt != nil {
valueTextView.linkTextAttributes = [
.foregroundColor: UIColor.systemGreen as Any,
.underlineColor: UIColor.clear]
}
let valueFont = UIFont.preferredFont(forTextStyle: verifiedAt == nil ? .body : .headline)
let mutableValue = NSMutableAttributedString(attributedString: value)
let valueRange = NSRange(location: 0, length: mutableValue.length)
mutableValue.removeAttribute(.font, range: valueRange)
mutableValue.addAttributes(
[.font: valueFont as Any,
.foregroundColor: UIColor.label],
range: valueRange)
mutableValue.insert(emojis: emojis, view: valueTextView, identityContext: identityContext)
mutableValue.resizeAttachments(toLineHeight: valueFont.lineHeight)
valueTextView.attributedText = mutableValue
valueTextView.textAlignment = .center
checkButton.setImage(
UIImage(
systemName: "checkmark",
withConfiguration: UIImage.SymbolConfiguration(scale: .small)),
for: .normal)
addSubview(checkButton)
checkButton.translatesAutoresizingMaskIntoConstraints = false
checkButton.tintColor = .systemGreen
checkButton.isHidden = verifiedAt == nil
checkButton.showsMenuAsPrimaryAction = true
checkButton.setContentCompressionResistancePriority(.required, for: .horizontal)
checkButton.setContentHuggingPriority(.required, for: .horizontal)
if let verifiedAt = verifiedAt {
checkButton.menu = UIMenu(
title: String.localizedStringWithFormat(
NSLocalizedString("account.field.verified", comment: ""),
Self.dateFormatter.string(from: verifiedAt)),
options: .displayInline,
children: [UIAction(title: NSLocalizedString("ok", comment: "")) { _ in }])
}
let nameLabelBottomConstraint = nameLabel.bottomAnchor.constraint(
equalTo: bottomAnchor,
constant: -.defaultSpacing)
let valueTextViewBottomConstraint = valueTextView.bottomAnchor.constraint(
lessThanOrEqualTo: bottomAnchor,
constant: -.defaultSpacing)
valueTextViewTrailingConstraint = valueTextView.trailingAnchor.constraint(
equalTo: trailingAnchor,
constant: -.defaultSpacing)
for constraint in [nameLabelBottomConstraint, valueTextViewBottomConstraint] {
constraint.priority = .justBelowMax
}
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .defaultSpacing),
nameLabel.topAnchor.constraint(equalTo: topAnchor, constant: .defaultSpacing),
nameLabelBottomConstraint,
dividerView.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: .defaultSpacing),
dividerView.topAnchor.constraint(equalTo: topAnchor),
dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
dividerView.widthAnchor.constraint(equalToConstant: .hairline),
checkButton.leadingAnchor.constraint(equalTo: dividerView.trailingAnchor, constant: .defaultSpacing),
valueTextView.leadingAnchor.constraint(
equalTo: verifiedAt == nil ? dividerView.trailingAnchor : checkButton.trailingAnchor,
constant: .defaultSpacing),
valueTextView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: .defaultSpacing),
valueTextViewTrailingConstraint!,
valueTextViewBottomConstraint,
nameLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / 3),
checkButton.centerYAnchor.constraint(equalTo: valueTextView.centerYAnchor),
valueTextView.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor),
nameBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
nameBackgroundView.topAnchor.constraint(equalTo: topAnchor),
nameBackgroundView.trailingAnchor.constraint(equalTo: dividerView.leadingAnchor),
nameBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
valueBackgroundView.leadingAnchor.constraint(equalTo: dividerView.trailingAnchor),
valueBackgroundView.topAnchor.constraint(equalTo: topAnchor),
valueBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
valueBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
let accessibilityAttributedLabel = NSMutableAttributedString(attributedString: mutableName)
accessibilityAttributedLabel.appendWithSeparator(mutableValue)
if let verifiedAt = verifiedAt {
accessibilityAttributedLabel.appendWithSeparator(
String.localizedStringWithFormat(
NSLocalizedString("account.field.verified", comment: ""),
Self.dateFormatter.string(from: verifiedAt)))
}
isAccessibilityElement = true
var accessibilityCustomActions = [UIAccessibilityCustomAction]()
mutableValue.enumerateAttribute(
.link,
in: NSRange(location: 0, length: mutableValue.length),
options: []) { attribute, range, _ in
guard let url = attribute as? URL else { return }
accessibilityCustomActions.append(
UIAccessibilityCustomAction(
name: String.localizedStringWithFormat(
NSLocalizedString("accessibility.activate-link-%@", comment: ""),
mutableValue.attributedSubstring(from: range).string)) { [weak self] _ in
guard let valueTextView = self?.valueTextView else { return false }
_ = valueTextView.delegate?.textView?(
valueTextView,
shouldInteractWith: url,
in: range,
interaction: .invokeDefaultAction)
return true
})
}
self.accessibilityAttributedLabel = accessibilityAttributedLabel
self.accessibilityCustomActions = accessibilityCustomActions
configureUserInteractionEnabledForAccessibility()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if !checkButton.isHidden {
valueTextViewTrailingConstraint?.constant = -.defaultSpacing * 2 - checkButton.frame.width
}
}
}
private extension AccountFieldView {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .full
return formatter
}()
func configureUserInteractionEnabledForAccessibility() {
valueTextView.isUserInteractionEnabled = !UIAccessibility.isVoiceOverRunning
}
}