// // 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) } }