jsdw_ios/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift

373 lines
14 KiB
Swift

//
// GroupMemberListView.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import UIKit
final class GroupMemberListView: UIView {
private var sectionTitles: [String] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F7F7F8")
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateSectionTitles(_ titles: [String]) {
sectionTitles = titles
}
func updateSearchPlaceholder(isHidden: Bool) {
searchPlaceholderStack.isHidden = isHidden
}
private func setupUI() {
addSubview(navView)
addSubview(searchField)
addSubview(tableView)
searchField.addSubview(searchPlaceholderStack)
[navView, searchField, tableView, searchPlaceholderStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
navView.topAnchor.constraint(equalTo: topAnchor),
navView.leadingAnchor.constraint(equalTo: leadingAnchor),
navView.trailingAnchor.constraint(equalTo: trailingAnchor),
navView.heightAnchor.constraint(equalToConstant: kNaviHeight),
searchField.topAnchor.constraint(equalTo: navView.bottomAnchor, constant: 20),
searchField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
searchField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
searchField.heightAnchor.constraint(equalToConstant: 38),
searchPlaceholderStack.centerXAnchor.constraint(equalTo: searchField.centerXAnchor),
searchPlaceholderStack.centerYAnchor.constraint(equalTo: searchField.centerYAnchor),
tableView.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 20),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
lazy var navView: BaseNavigationView = BaseNavigationView(title: "圈子成员")
lazy var searchField: UISearchTextField = {
let textField = UISearchTextField()
textField.backgroundColor = .white
textField.borderStyle = .none
textField.layer.cornerRadius = 12
textField.clipsToBounds = true
textField.font = .systemFont(ofSize: 15, weight: .regular)
textField.textColor = UIColor(hexStr: "#293445")
textField.tintColor = UIColor(hexStr: "#293445")
textField.returnKeyType = .search
textField.clearButtonMode = .whileEditing
textField.leftView = UIView(frame: .zero)
textField.leftViewMode = .never
return textField
}()
private lazy var searchPlaceholderStack: UIStackView = {
let imageView = UIImageView(image: UIImage(named: "Group/remove_member_search"))
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: 16).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 16).isActive = true
let label = UILabel()
label.text = "搜索"
label.textColor = UIColor(hexStr: "#B0B0B5")
label.font = .systemFont(ofSize: 15, weight: .regular)
let stack = UIStackView(arrangedSubviews: [imageView, label])
stack.axis = .horizontal
stack.alignment = .center
stack.spacing = 6
stack.isUserInteractionEnabled = false
return stack
}()
lazy var tableView: UITableView = {
let view = UITableView(frame: .zero, style: .plain)
view.backgroundColor = .clear
view.separatorStyle = .none
view.rowHeight = GroupMemberDirectoryCell.rowHeight
view.estimatedRowHeight = GroupMemberDirectoryCell.rowHeight
view.sectionHeaderHeight = 42
view.estimatedSectionHeaderHeight = 42
view.showsVerticalScrollIndicator = false
view.keyboardDismissMode = .onDrag
view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin + 24, right: 0)
if #available(iOS 15.0, *) {
view.sectionHeaderTopPadding = 0
}
view.register(GroupMemberDirectoryCell.self)
return view
}()
}
extension GroupMemberListView: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard section < sectionTitles.count else { return nil }
let container = UIView()
let label = UILabel()
label.text = sectionTitles[section]
label.textColor = UIColor(hexStr: "#777A80")
label.font = .systemFont(ofSize: 14, weight: .regular)
container.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8)
])
return container
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
42
}
}
final class GroupMemberDirectoryCell: UITableViewCell {
static let rowHeight: CGFloat = 52
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(_ member: GroupMemberModel,
showsSeparator: Bool,
roundsTopCorners: Bool,
roundsBottomCorners: Bool) {
avatarView.image = member.userIcon
nicknameLabel.text = member.nick_name.isEmpty ? "未知昵称" : member.nick_name
separatorView.isHidden = !showsSeparator
var corners: CACornerMask = []
if roundsTopCorners {
corners.formUnion([.layerMinXMinYCorner, .layerMaxXMinYCorner])
}
if roundsBottomCorners {
corners.formUnion([.layerMinXMaxYCorner, .layerMaxXMaxYCorner])
}
cardView.layer.maskedCorners = corners
}
private func setupUI() {
contentView.addSubview(cardView)
cardView.addSubview(avatarView)
cardView.addSubview(nicknameLabel)
cardView.addSubview(separatorView)
[cardView, avatarView, nicknameLabel, separatorView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
avatarView.centerYAnchor.constraint(equalTo: cardView.centerYAnchor),
avatarView.widthAnchor.constraint(equalToConstant: 38),
avatarView.heightAnchor.constraint(equalToConstant: 38),
nicknameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
nicknameLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16),
nicknameLabel.centerYAnchor.constraint(equalTo: cardView.centerYAnchor),
separatorView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 20),
separatorView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -13),
separatorView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 0.5)
])
}
private lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}()
private lazy var avatarView: UIImageView = {
let view = UIImageView()
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 10
view.clipsToBounds = true
return view
}()
private lazy var nicknameLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#111111")
label.font = .systemFont(ofSize: 16, weight: .regular)
label.lineBreakMode = .byTruncatingTail
return label
}()
private lazy var separatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EDEEF0")
return view
}()
}
// This compact cell is shared by the map and member information pages.
final class GroupMemberListCell: UICollectionViewCell {
private static let sosBlinkKey = "sosBlink"
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
updateSOSBlink(false)
bubbleImageView.isHidden = true
}
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
avatarImageView.image = model.userIcon
nameLabel.text = model.nick_name
nameLabel.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
selectionImageView.isHidden = !isSelected
vipBackgroundView.isHidden = model.level == 1
let isSOS = model.status == 1
updateSOSBlink(isSOS)
bubbleImageView.isHidden = isSOS || model.status != 2
}
private func updateSOSBlink(_ blinking: Bool) {
sosImageView.layer.removeAnimation(forKey: Self.sosBlinkKey)
sosImageView.isHidden = !blinking
sosImageView.alpha = 1
guard blinking else { return }
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 1
animation.toValue = 0.2
animation.duration = 0.6
animation.autoreverses = true
animation.repeatCount = .infinity
sosImageView.layer.add(animation, forKey: Self.sosBlinkKey)
}
private func setupUI() {
contentView.addSubview(selectionImageView)
contentView.addSubview(avatarImageView)
contentView.addSubview(bubbleImageView)
contentView.addSubview(sosImageView)
contentView.addSubview(vipBackgroundView)
contentView.addSubview(nameLabel)
[selectionImageView, avatarImageView, bubbleImageView, sosImageView, vipBackgroundView, nameLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
selectionImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
selectionImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
selectionImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
selectionImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 9),
avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 50),
avatarImageView.heightAnchor.constraint(equalToConstant: 50),
bubbleImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
bubbleImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
bubbleImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
bubbleImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
sosImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
sosImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
sosImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
sosImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
vipBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7),
vipBackgroundView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 7),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
}
private lazy var selectionImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg"))
view.contentMode = .scaleAspectFill
view.isHidden = true
return view
}()
private lazy var avatarImageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 25
view.clipsToBounds = true
return view
}()
private lazy var bubbleImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "GroupMemberList/bubble"))
view.contentMode = .scaleAspectFit
view.isHidden = true
return view
}()
private lazy var sosImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "GroupMemberList/sos"))
view.contentMode = .scaleAspectFit
view.isHidden = true
return view
}()
private lazy var vipBackgroundView: UIImageView = {
let view = UIImageView(image: UIImage(named: "GroupMemberList/vip"))
view.isHidden = true
return view
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textAlignment = .center
label.lineBreakMode = .byTruncatingTail
return label
}()
}