422 lines
13 KiB
Swift
422 lines
13 KiB
Swift
//
|
||
// GroupListPopView.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/5/30.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
class GroupListPopView: UIView {
|
||
|
||
private static let shared = GroupListPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||
|
||
private static let rowHeight: CGFloat = 72
|
||
private static let maxVisibleRows = 3
|
||
private static let primaryBlue = UIColor(hexStr: "#16B3FF")
|
||
private static let cardBg = UIColor(hexStr: "#F5FBFF")
|
||
private static let tagBg = UIColor(hexStr: "#FFF9DD")
|
||
private static let tagText = UIColor(hexStr: "#D5923F")
|
||
|
||
private var groupModel: GroupModel? {
|
||
didSet {
|
||
guard let model = groupModel else { return }
|
||
selectedGroupKey = model.default_group_key
|
||
groupList = model.groups
|
||
updateTableHeight()
|
||
}
|
||
}
|
||
|
||
private var selectedGroupKey: String = ""
|
||
|
||
private var groupList: [GroupInfoModel] = [] {
|
||
didSet {
|
||
tableView.reloadData()
|
||
}
|
||
}
|
||
|
||
/// 完成选中进行回调
|
||
private var completion: ((String?) -> Void)?
|
||
|
||
@objc private func tapMask() {
|
||
completion?(nil)
|
||
}
|
||
|
||
@objc private func cancelAction() {
|
||
completion?(nil)
|
||
}
|
||
|
||
@objc private func confirmAction() {
|
||
completion?(selectedGroupKey.isEmpty ? nil : selectedGroupKey)
|
||
}
|
||
|
||
private lazy var bgView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .black.withAlphaComponent(0.5)
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
lazy var infoView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .clear
|
||
|
||
let bg = UIView()
|
||
bg.backgroundColor = UIColor(hexStr: "#FDFDFD")
|
||
view.addSubview(bg)
|
||
bg.layoutChain
|
||
.top(80)
|
||
.edges(excludingEdge: .top)
|
||
|
||
return view
|
||
}()
|
||
|
||
lazy var headerImageView: UIImageView = {
|
||
let view = UIImageView()
|
||
view.image = UIImage(named: "Home/group_list_header")
|
||
view.contentMode = .scaleAspectFill
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
lazy var tableView: UITableView = {
|
||
let tableView = UITableView(frame: .zero, style: .plain)
|
||
tableView.backgroundColor = .clear
|
||
tableView.separatorStyle = .none
|
||
tableView.rowHeight = Self.rowHeight
|
||
tableView.estimatedRowHeight = Self.rowHeight
|
||
tableView.showsVerticalScrollIndicator = false
|
||
tableView.bounces = false
|
||
tableView.register(GroupListCell.self)
|
||
tableView.dataSource = self
|
||
tableView.delegate = self
|
||
return tableView
|
||
}()
|
||
|
||
lazy var cancelBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setTitle("取消", for: .normal)
|
||
btn.setTitleColor(Self.primaryBlue, for: .normal)
|
||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||
btn.backgroundColor = .white
|
||
btn.cornerRadius = 16
|
||
btn.layer.borderWidth = 1
|
||
btn.layer.borderColor = Self.primaryBlue.cgColor
|
||
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
lazy var confirmBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setTitle("确定", for: .normal)
|
||
btn.setTitleColor(.white, for: .normal)
|
||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||
btn.backgroundColor = Self.primaryBlue
|
||
btn.cornerRadius = 16
|
||
btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
// MARK: - Init
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
addSubview(bgView)
|
||
bgView.addSubview(infoView)
|
||
infoView.addSubview(headerImageView)
|
||
infoView.addSubview(tableView)
|
||
infoView.addSubview(cancelBtn)
|
||
infoView.addSubview(confirmBtn)
|
||
|
||
bgView.layoutChain.edges()
|
||
|
||
infoView.layoutChain
|
||
.bottom()
|
||
.edgesHorzontal()
|
||
|
||
headerImageView.layoutChain
|
||
.top()
|
||
.edgesHorzontal()
|
||
.heightToWidth(127/375)
|
||
|
||
cancelBtn.layoutChain
|
||
.left(20)
|
||
.bottom(20 + kSafeBottomMargin)
|
||
.height(44)
|
||
.widthToView(confirmBtn)
|
||
|
||
confirmBtn.layoutChain
|
||
.leftToRightOfView(cancelBtn, offset: 12)
|
||
.right(20)
|
||
.bottomToView(cancelBtn)
|
||
.height(44)
|
||
|
||
tableView.layoutChain
|
||
.topToBottomOfView(headerImageView, offset: -20)
|
||
.edgesHorzontal(12)
|
||
.height(Self.rowHeight)
|
||
.bottomToTopOfView(cancelBtn, offset: -16)
|
||
|
||
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
|
||
tap.delegate = self
|
||
addGestureRecognizer(tap)
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
private func updateTableHeight() {
|
||
let visible = min(groupList.count, Self.maxVisibleRows)
|
||
let height = CGFloat(visible) * Self.rowHeight
|
||
tableView.layoutChain.height(height)
|
||
tableView.isScrollEnabled = groupList.count > Self.maxVisibleRows
|
||
tableView.reloadData()
|
||
}
|
||
|
||
private func slideOffTransform() -> CGAffineTransform {
|
||
layoutIfNeeded()
|
||
let offset = infoView.bounds.height// + 20 + kSafeBottomMargin
|
||
return CGAffineTransform(translationX: 0, y: max(offset, kScreenHeight * 0.5))
|
||
}
|
||
}
|
||
|
||
// MARK: - Public
|
||
extension GroupListPopView {
|
||
|
||
/// 通过 window 从底部弹出圈子列表
|
||
static func show(groupModel: GroupModel,
|
||
completion: @escaping ((String?) -> Void)) {
|
||
guard let superView = kKeyWindow else { return }
|
||
|
||
let pop = GroupListPopView.shared
|
||
if pop.superview != nil {
|
||
pop.removeFromSuperview()
|
||
}
|
||
|
||
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
||
pop.groupModel = groupModel
|
||
pop.completion = { text in
|
||
completion(text)
|
||
GroupListPopView.dismiss()
|
||
}
|
||
|
||
pop.bgView.alpha = 0
|
||
superView.addSubview(pop)
|
||
superView.bringSubviewToFront(pop)
|
||
pop.layoutIfNeeded()
|
||
pop.infoView.transform = pop.slideOffTransform()
|
||
|
||
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
||
pop.bgView.alpha = 1
|
||
pop.infoView.transform = .identity
|
||
}
|
||
}
|
||
|
||
/// 关闭(滑出底部)
|
||
static func dismiss() {
|
||
let pop = GroupListPopView.shared
|
||
guard pop.superview != nil else { return }
|
||
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
||
pop.bgView.alpha = 0
|
||
pop.infoView.transform = pop.slideOffTransform()
|
||
} completion: { _ in
|
||
pop.infoView.transform = .identity
|
||
pop.removeFromSuperview()
|
||
}
|
||
}
|
||
}
|
||
// MARK: - UIGestureRecognizerDelegate
|
||
extension GroupListPopView: UIGestureRecognizerDelegate {
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||
if let view = touch.view, !(view == self || view == bgView) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||
extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
groupList.count
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
||
let model = groupList[indexPath.row]
|
||
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key)
|
||
return cell
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
selectedGroupKey = groupList[indexPath.row].group_key
|
||
tableView.reloadData()
|
||
}
|
||
}
|
||
|
||
// MARK: - GroupListCell
|
||
class GroupListCell: UITableViewCell {
|
||
|
||
func configure(model: GroupInfoModel, isSelected: Bool) {
|
||
avaterImgView.image = model.groupIcon
|
||
nameLab.text = model.name
|
||
countLab.text = "共\(model.people_no)成员"
|
||
|
||
tagStack.arrangedSubviews.forEach {
|
||
tagStack.removeArrangedSubview($0)
|
||
$0.removeFromSuperview()
|
||
}
|
||
let labels = Array(model.labels.prefix(2))
|
||
tagStack.isHidden = labels.isEmpty
|
||
for text in labels {
|
||
tagStack.addArrangedSubview(Self.makeTagLabel(text))
|
||
}
|
||
|
||
cardView.backgroundColor = isSelected ? GroupListPopView.cardBgColor : .white
|
||
cardView.layer.borderWidth = isSelected ? 1 : 0
|
||
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
|
||
|
||
checkIcon.image = isSelected
|
||
? UIImage(systemName: "checkmark.circle.fill")
|
||
: UIImage(systemName: "circle")
|
||
checkIcon.tintColor = isSelected
|
||
? GroupListPopView.primaryBlueColor
|
||
: UIColor(hexStr: "#D0D0D0")
|
||
}
|
||
|
||
private static func makeTagLabel(_ text: String) -> UILabel {
|
||
let label = PaddingLabel()
|
||
label.text = text
|
||
label.textColor = GroupListPopView.tagTextColor
|
||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||
label.backgroundColor = GroupListPopView.tagBgColor
|
||
label.cornerRadius = 4
|
||
label.clipsToBounds = true
|
||
label.textAlignment = .center
|
||
return label
|
||
}
|
||
|
||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||
selectionStyle = .none
|
||
backgroundColor = .clear
|
||
contentView.backgroundColor = .clear
|
||
setupSubviews()
|
||
}
|
||
|
||
private func setupSubviews() {
|
||
contentView.addSubview(cardView)
|
||
cardView.addSubview(avaterImgView)
|
||
cardView.addSubview(nameLab)
|
||
cardView.addSubview(tagStack)
|
||
cardView.addSubview(countLab)
|
||
cardView.addSubview(checkIcon)
|
||
|
||
cardView.layoutChain
|
||
.edgesVertical(4)
|
||
.edgesHorzontal(4)
|
||
|
||
avaterImgView.layoutChain
|
||
.left(12)
|
||
.centerY()
|
||
.width(44)
|
||
.height(44)
|
||
|
||
checkIcon.layoutChain
|
||
.right(12)
|
||
.centerY()
|
||
.width(22)
|
||
.height(22)
|
||
|
||
nameLab.layoutChain
|
||
.topToView(avaterImgView, offset: 2)
|
||
.leftToRightOfView(avaterImgView, offset: 10)
|
||
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
||
|
||
tagStack.layoutChain
|
||
.leftToRightOfView(nameLab, offset: 6)
|
||
.centerY(nameLab)
|
||
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
||
|
||
countLab.layoutChain
|
||
.topToBottomOfView(nameLab, offset: 4)
|
||
.leftToView(nameLab)
|
||
.rightToLeftOfView(checkIcon, offset: -8)
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
lazy var cardView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .white
|
||
view.cornerRadius = 12
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
lazy var avaterImgView: UIImageView = {
|
||
let view = UIImageView()
|
||
view.contentMode = .scaleAspectFill
|
||
view.cornerRadius = 8
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
lazy var nameLab: UILabel = {
|
||
let label = UILabel()
|
||
label.textColor = UIColor(hexStr: "#353B4F")
|
||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||
return label
|
||
}()
|
||
|
||
lazy var tagStack: UIStackView = {
|
||
let view = UIStackView()
|
||
view.axis = .horizontal
|
||
view.alignment = .center
|
||
view.spacing = 4
|
||
view.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||
return view
|
||
}()
|
||
|
||
lazy var countLab: UILabel = {
|
||
let label = UILabel()
|
||
label.textColor = UIColor(hexStr: "#999999")
|
||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||
return label
|
||
}()
|
||
|
||
lazy var checkIcon: UIImageView = {
|
||
let view = UIImageView()
|
||
view.contentMode = .scaleAspectFit
|
||
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
|
||
return view
|
||
}()
|
||
}
|
||
|
||
// MARK: - Color accessors for cell (file-private via static)
|
||
private extension GroupListPopView {
|
||
static var primaryBlueColor: UIColor { primaryBlue }
|
||
static var cardBgColor: UIColor { cardBg }
|
||
static var tagBgColor: UIColor { tagBg }
|
||
static var tagTextColor: UIColor { tagText }
|
||
}
|
||
|
||
/// 带内边距的标签
|
||
private class PaddingLabel: UILabel {
|
||
private let inset = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
|
||
|
||
override func drawText(in rect: CGRect) {
|
||
super.drawText(in: rect.inset(by: inset))
|
||
}
|
||
|
||
override var intrinsicContentSize: CGSize {
|
||
let size = super.intrinsicContentSize
|
||
return CGSize(width: size.width + inset.left + inset.right,
|
||
height: size.height + inset.top + inset.bottom)
|
||
}
|
||
}
|