jsdw_ios/QuickLocation/Section/Group/Mailbox/MailboxView.swift

495 lines
21 KiB
Swift

//
// MailboxView.swift
// QuickLocation
//
import UIKit
import MJRefresh
extension MailboxCategory {
var tintColor: UIColor {
switch self {
case .interaction:
return UIColor(hexStr: "#3ED989")
case .notice:
return UIColor(hexStr: "#C274F7")
case .system:
return UIColor(hexStr: "#55B8F7")
}
}
var backgroundColor: UIColor {
switch self {
case .interaction:
return UIColor(hexStr: "#E7FFF2")
case .notice:
return UIColor(hexStr: "#F9E9FF")
case .system:
return UIColor(hexStr: "#E8F8FF")
}
}
var iconAssetName: String {
switch self {
case .interaction:
return "Mailbox/interaction"
case .notice:
return "Mailbox/notice"
case .system:
return "Mailbox/system"
}
}
var listIconAssetName: String {
switch self {
case .system:
return "Mailbox/system_message"
default:
return iconAssetName
}
}
}
final class MailboxView: UIView {
let backButton = UIButton(type: .custom)
let clearButton = UIButton(type: .custom)
let tableView = UITableView(frame: .zero, style: .plain)
private let headerGradientLayer = CAGradientLayer()
private let titleLabel = UILabel()
private let categoryStackView = UIStackView()
private let contentTopSeparator = UIView()
private let emptyView = UIView()
private let emptyImageView = UIImageView(image: UIImage(named: "empty_data"))
private let emptyLabel = UILabel()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private var categoryViews: [MailboxCategory: MailboxCategoryView] = [:]
var onSelectCategory: ((MailboxCategory) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
headerGradientLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: kNaviHeight + 132)
}
func selectCategory(_ category: MailboxCategory) {
categoryViews.forEach { itemCategory, categoryView in
categoryView.setSelected(itemCategory == category)
}
}
func updateUnreadCounts(_ counts: [MailboxCategory: Int]) {
categoryViews.forEach { category, categoryView in
categoryView.setUnreadCount(counts[category] ?? 0)
}
}
func updateState(hasMessages: Bool, isLoading: Bool, reachedEnd: Bool) {
loadingIndicator.isHidden = !isLoading
if isLoading {
loadingIndicator.startAnimating()
} else {
loadingIndicator.stopAnimating()
}
emptyImageView.isHidden = isLoading || hasMessages || !reachedEnd
emptyLabel.isHidden = emptyImageView.isHidden
tableView.mj_footer?.isHidden = !hasMessages || reachedEnd
if reachedEnd {
tableView.mj_footer?.endRefreshingWithNoMoreData()
} else {
tableView.mj_footer?.resetNoMoreData()
tableView.mj_footer?.endRefreshing()
}
}
private func setupUI() {
backgroundColor = .white
headerGradientLayer.colors = [
UIColor(hexStr: "#DFF9FF").cgColor,
UIColor(hexStr: "#F3FFE9").cgColor,
UIColor.white.cgColor
]
headerGradientLayer.locations = [0, 0.58, 1]
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
headerGradientLayer.endPoint = CGPoint(x: 1, y: 1)
layer.insertSublayer(headerGradientLayer, at: 0)
setupNavigationBar()
setupCategories()
setupTableView()
setupEmptyView()
}
private func setupNavigationBar() {
titleLabel.text = "消息"
titleLabel.textColor = UIColor(hexStr: "#18304B")
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
titleLabel.textAlignment = .center
configureNavigationButton(backButton, symbolName: "chevron.left")
configureNavigationButton(clearButton, symbolName: "trash.fill")
backButton.accessibilityLabel = "返回"
clearButton.accessibilityLabel = "清空全部消息"
addSubview(titleLabel)
addSubview(backButton)
addSubview(clearButton)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.translatesAutoresizingMaskIntoConstraints = false
clearButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
backButton.topAnchor.constraint(equalTo: topAnchor, constant: kStatusBarHeight + 8),
backButton.widthAnchor.constraint(equalToConstant: 36),
backButton.heightAnchor.constraint(equalToConstant: 36),
clearButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
clearButton.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
clearButton.widthAnchor.constraint(equalToConstant: 36),
clearButton.heightAnchor.constraint(equalToConstant: 36),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor)
])
}
private func configureNavigationButton(_ button: UIButton, symbolName: String) {
let configuration = UIImage.SymbolConfiguration(pointSize: 18, weight: .bold)
button.setImage(UIImage(systemName: symbolName, withConfiguration: configuration), for: .normal)
button.tintColor = UIColor(hexStr: "#213148")
button.backgroundColor = UIColor.white.withAlphaComponent(0.88)
button.layer.cornerRadius = 10
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
button.layer.shadowOpacity = 1
button.layer.shadowRadius = 8
button.layer.shadowOffset = CGSize(width: 0, height: 3)
}
private func setupCategories() {
categoryStackView.axis = .horizontal
categoryStackView.distribution = .fillEqually
categoryStackView.alignment = .fill
MailboxCategory.displayOrder.forEach { category in
let categoryView = MailboxCategoryView(category: category)
categoryView.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
categoryStackView.addArrangedSubview(categoryView)
categoryViews[category] = categoryView
}
contentTopSeparator.backgroundColor = UIColor(hexStr: "#E8E8E8")
addSubview(categoryStackView)
addSubview(contentTopSeparator)
categoryStackView.translatesAutoresizingMaskIntoConstraints = false
contentTopSeparator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
categoryStackView.topAnchor.constraint(equalTo: topAnchor, constant: kNaviHeight + 14),
categoryStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
categoryStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
categoryStackView.heightAnchor.constraint(equalToConstant: 112),
contentTopSeparator.topAnchor.constraint(equalTo: categoryStackView.bottomAnchor),
contentTopSeparator.leadingAnchor.constraint(equalTo: leadingAnchor),
contentTopSeparator.trailingAnchor.constraint(equalTo: trailingAnchor),
contentTopSeparator.heightAnchor.constraint(equalToConstant: 0.5)
])
}
private func setupTableView() {
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = 66
tableView.showsVerticalScrollIndicator = false
tableView.tableFooterView = UIView()
tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: kSafeBottomMargin + 18, right: 0)
tableView.register(MailboxMessageCell.self, forCellReuseIdentifier: MailboxMessageCell.reuseIdentifier)
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
private func setupEmptyView() {
emptyView.isUserInteractionEnabled = false
emptyImageView.contentMode = .scaleAspectFit
emptyLabel.text = "这里怎么什么都没有~"
emptyLabel.textColor = UIColor(hexStr: "#B1B1B1")
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textAlignment = .center
loadingIndicator.color = UIColor(hexStr: "#16B3FF")
addSubview(emptyView)
emptyView.addSubview(emptyImageView)
emptyView.addSubview(emptyLabel)
emptyView.addSubview(loadingIndicator)
emptyView.translatesAutoresizingMaskIntoConstraints = false
emptyImageView.translatesAutoresizingMaskIntoConstraints = false
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
emptyView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
emptyView.leadingAnchor.constraint(equalTo: leadingAnchor),
emptyView.trailingAnchor.constraint(equalTo: trailingAnchor),
emptyView.bottomAnchor.constraint(equalTo: bottomAnchor),
emptyImageView.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
emptyImageView.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -56),
emptyImageView.widthAnchor.constraint(equalToConstant: 140),
emptyImageView.heightAnchor.constraint(equalToConstant: 120),
emptyLabel.topAnchor.constraint(equalTo: emptyImageView.bottomAnchor, constant: 8),
emptyLabel.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
loadingIndicator.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40)
])
}
@objc private func categoryTapped(_ sender: MailboxCategoryView) {
onSelectCategory?(sender.category)
}
}
final class MailboxCategoryView: UIControl {
let category: MailboxCategory
private let iconBackgroundView = UIView()
private let iconImageView = UIImageView()
private let titleLabel = UILabel()
private let underlineView = UIView()
private let badgeLabel = UILabel()
init(category: MailboxCategory) {
self.category = category
super.init(frame: .zero)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSelected(_ selected: Bool) {
titleLabel.textColor = UIColor(hexStr: "#26364A")
underlineView.isHidden = !selected
}
func setUnreadCount(_ count: Int) {
badgeLabel.isHidden = count <= 0
badgeLabel.text = count > 99 ? "99+" : "\(count)"
}
private func setupUI() {
accessibilityLabel = category.title
iconBackgroundView.isUserInteractionEnabled = false
iconBackgroundView.backgroundColor = .clear
iconImageView.image = UIImage(named: category.iconAssetName)
iconImageView.contentMode = .scaleAspectFit
titleLabel.text = category.title
titleLabel.textAlignment = .center
titleLabel.textColor = UIColor(hexStr: "#26364A")
titleLabel.font = .systemFont(ofSize: 15)
underlineView.backgroundColor = UIColor(hexStr: "#13B6FB")
underlineView.layer.cornerRadius = 1.5
underlineView.isHidden = true
badgeLabel.backgroundColor = UIColor(hexStr: "#FF5260")
badgeLabel.textColor = .white
badgeLabel.font = .systemFont(ofSize: 10, weight: .medium)
badgeLabel.textAlignment = .center
badgeLabel.layer.cornerRadius = 9
badgeLabel.clipsToBounds = true
badgeLabel.isHidden = true
addSubview(iconBackgroundView)
iconBackgroundView.addSubview(iconImageView)
addSubview(titleLabel)
addSubview(underlineView)
addSubview(badgeLabel)
[iconBackgroundView, iconImageView, titleLabel, underlineView, badgeLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
iconBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
iconBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
iconBackgroundView.widthAnchor.constraint(equalToConstant: 50),
iconBackgroundView.heightAnchor.constraint(equalToConstant: 50),
iconImageView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 50),
iconImageView.heightAnchor.constraint(equalToConstant: 50),
titleLabel.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: 7),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
underlineView.bottomAnchor.constraint(equalTo: bottomAnchor),
underlineView.centerXAnchor.constraint(equalTo: centerXAnchor),
underlineView.widthAnchor.constraint(equalToConstant: 18),
underlineView.heightAnchor.constraint(equalToConstant: 3),
badgeLabel.centerXAnchor.constraint(equalTo: iconBackgroundView.trailingAnchor, constant: -2),
badgeLabel.centerYAnchor.constraint(equalTo: iconBackgroundView.topAnchor, constant: 2),
badgeLabel.heightAnchor.constraint(equalToConstant: 18),
badgeLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 18)
])
}
}
final class MailboxMessageCell: UITableViewCell {
static let reuseIdentifier = "MailboxMessageCell"
private let avatarView = UIImageView()
private let defaultIconView = MailboxTypeIconView()
private let unreadDotView = UIView()
private let titleLabel = UILabel()
private let contentLabel = UILabel()
private let timeLabel = UILabel()
private let separatorView = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.setHeadPic(nil)
}
func configure(message: MailboxMessage) {
titleLabel.text = message.title
contentLabel.text = Self.previewText(for: message)
timeLabel.text = Self.timeText(from: message.modifyTime)
unreadDotView.isHidden = message.isRead
let senderIcon = message.senderIcon.trimmingCharacters(in: .whitespacesAndNewlines)
let shouldShowAvatar = message.category != .system && !senderIcon.isEmpty && senderIcon != "0"
avatarView.isHidden = !shouldShowAvatar
defaultIconView.isHidden = shouldShowAvatar
if shouldShowAvatar {
avatarView.setHeadPic(senderIcon)
} else {
defaultIconView.configure(category: message.category)
}
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .white
avatarView.contentMode = .scaleAspectFill
avatarView.layer.cornerRadius = 11
avatarView.clipsToBounds = true
unreadDotView.backgroundColor = UIColor(hexStr: "#FF5260")
unreadDotView.layer.cornerRadius = 4
titleLabel.font = .systemFont(ofSize: 15, weight: .medium)
titleLabel.textColor = UIColor(hexStr: "#26364A")
titleLabel.numberOfLines = 1
contentLabel.font = .systemFont(ofSize: 13)
contentLabel.textColor = UIColor(hexStr: "#AAAAAA")
contentLabel.numberOfLines = 1
timeLabel.font = .systemFont(ofSize: 12)
timeLabel.textColor = UIColor(hexStr: "#BBBBBB")
timeLabel.textAlignment = .right
separatorView.backgroundColor = UIColor(hexStr: "#EEEEEE")
contentView.addSubview(avatarView)
contentView.addSubview(defaultIconView)
contentView.addSubview(unreadDotView)
contentView.addSubview(titleLabel)
contentView.addSubview(contentLabel)
contentView.addSubview(timeLabel)
contentView.addSubview(separatorView)
[avatarView, defaultIconView, unreadDotView, titleLabel, contentLabel, timeLabel, separatorView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 26),
avatarView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
avatarView.widthAnchor.constraint(equalToConstant: 42),
avatarView.heightAnchor.constraint(equalToConstant: 42),
defaultIconView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
defaultIconView.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
defaultIconView.widthAnchor.constraint(equalTo: avatarView.widthAnchor),
defaultIconView.heightAnchor.constraint(equalTo: avatarView.heightAnchor),
unreadDotView.centerXAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: -2),
unreadDotView.centerYAnchor.constraint(equalTo: avatarView.topAnchor, constant: 2),
unreadDotView.widthAnchor.constraint(equalToConstant: 8),
unreadDotView.heightAnchor.constraint(equalToConstant: 8),
timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
timeLabel.widthAnchor.constraint(equalToConstant: 46),
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: timeLabel.leadingAnchor, constant: -8),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
contentLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 0.5)
])
}
private static func previewText(for message: MailboxMessage) -> String {
guard message.category == .interaction,
message.content.contains("]"),
let bracketIndex = message.content.lastIndex(of: "[") else {
return message.content
}
return String(message.content[..<bracketIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func timeText(from value: String) -> String {
guard value.count >= 16 else { return value }
let startIndex = value.index(value.startIndex, offsetBy: 11)
let endIndex = value.index(startIndex, offsetBy: 5, limitedBy: value.endIndex) ?? value.endIndex
return String(value[startIndex..<endIndex])
}
}
final class MailboxTypeIconView: UIView {
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 11
clipsToBounds = true
imageView.contentMode = .center
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
imageView.widthAnchor.constraint(equalTo: widthAnchor),
imageView.heightAnchor.constraint(equalTo: heightAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(category: MailboxCategory) {
backgroundColor = .clear
imageView.image = UIImage(named: category.listIconAssetName)
}
}