// // GroupView.swift // QuickLocation // // Created by 八条 on 2026/5/29. // import UIKit import RxSwift import RxCocoa import SDCycleScrollView // MARK: - PanScrollView(参考 PanScrollView,允许嵌套手势同时识别) class PanScrollView: UIScrollView { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } } class GroupView: UIView { var disposeBag = DisposeBag() /// 当前是否吸顶 private var isSticky = false /// 内部 tableview 是否可滚动 private var isSubCanScroll = false var stickThreshold: CGFloat { return segmentView.dl.y } // MARK: - Setup private func setupUI() { addSubview(navBgView) addSubview(navBarView) navBarView.addSubview(navTitleLabel) navBarView.addSubview(scanBtn) addSubview(mainScrollView) mainScrollView.addSubview(contentView) contentView.addSubview(cycleScrollView) contentView.addSubview(actionButtonsView) actionButtonsView.addSubview(createGroupBtn) actionButtonsView.addSubview(joinGroupBtn) contentView.addSubview(hotGroupTitleLabel) contentView.addSubview(hotGroupsCollectionView) contentView.addSubview(segmentView) segmentView.addSubview(createdTabLabel) segmentView.addSubview(joinedTabLabel) segmentView.addSubview(tabIndicator) contentView.addSubview(segmentScrollView) segmentScrollView.addSubview(segmentContentView) segmentContentView.addSubview(createdTableView) segmentContentView.addSubview(joinedTableView) navBgView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerX() scanBtn.layoutChain .right(15) .centerY(navTitleLabel) .width(24).height(24) mainScrollView.layoutChain .topToBottomOfView(navBgView) .edges(excludingEdge: .top) contentView.layoutChain .edges() .widthToView(mainScrollView) // 轮播图 cycleScrollView.layoutChain .top(10) .left(15).right(15) .heightToWidth(100/375) // 创建/加入圈子 actionButtonsView.layoutChain .topToBottomOfView(cycleScrollView, offset: 38) .edgesHorzontal() createGroupBtn.layoutChain .left(15) .edgesVertical() .widthToView(joinGroupBtn) .height(40) joinGroupBtn.layoutChain .topToView(createGroupBtn) .bottomToView(createGroupBtn) .leftToRightOfView(createGroupBtn, offset: 21) .right(15) .widthToView(joinGroupBtn) // 热门酷圈 hotGroupTitleLabel.layoutChain .topToBottomOfView(actionButtonsView, offset: 21) .left(16) hotGroupsCollectionView.layoutChain .topToBottomOfView(hotGroupTitleLabel, offset: 8) .edgesHorzontal() .height(88) // Segment let labelWidth = (UIScreen.main.bounds.width - 60) / 2 segmentView.layoutChain .topToBottomOfView(hotGroupsCollectionView) .edgesHorzontal() .height(44) createdTabLabel.layoutChain .left(30).centerY() .width(labelWidth) joinedTabLabel.layoutChain .right(30).centerY() .width(labelWidth) tabIndicator.layoutChain .bottom().centerX(createdTabLabel) .width(14).height(3) // 横向 segmentScrollView segmentScrollView.layoutChain .topToBottomOfView(segmentView) .edgesHorzontal() .bottom() .height(kScreenHeight - kNaviHeight - 44) segmentContentView.layoutChain .edges() .heightToView(segmentScrollView) let svWidth = UIScreen.main.bounds.width createdTableView.layoutChain .top().bottom().left() .width(svWidth) joinedTableView.layoutChain .top().bottom() .leftToRightOfView(createdTableView) .width(svWidth) } override func layoutSubviews() { super.layoutSubviews() segmentContentView.layoutChain.width(bounds.width * 2) } // MARK: - Nav lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_1") iv.contentMode = .scaleAspectFill return iv }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.text = "圈子" label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var scanBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Group/scan"), for: .normal) return btn }() // MARK: - Main Scroll lazy var mainScrollView: PanScrollView = { let sv = PanScrollView() sv.backgroundColor = .clear sv.delegate = self sv.showsVerticalScrollIndicator = false sv.bounces = false return sv }() lazy var contentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 轮播图 lazy var cycleScrollView: SDCycleScrollView = { let view = SDCycleScrollView(frame: .zero) view.backgroundColor = .lightGray view.scrollDirection = .horizontal view.showPageControl = true view.bannerImageViewContentMode = .scaleAspectFill view.autoScrollTimeInterval = 5 view.currentPageDotColor = UIColor(hexStr: "#16B3FF") view.pageDotColor = UIColor(hexStr: "#7AD6FF").withAlphaComponent(0.4) view.pageControlDotSize = CGSize(width: 8, height: 8) view.localizationImageNamesGroup = ["Mask_group", "Mask_group", "Mask_group"] view.clipsToBounds = false view.pageControlBottomOffset = -34 return view }() // MARK: - 创建 / 加入 圈子 private lazy var actionButtonsView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() lazy var createGroupBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("创建圈子", for: .normal) btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal) btn.cornerRadius = 20 return btn }() lazy var joinGroupBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("加入圈子", for: .normal) btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal) btn.cornerRadius = 20 return btn }() // MARK: - 热门酷圈 private lazy var hotGroupTitleLabel: UILabel = { let label = UILabel() label.text = "热门酷圈" label.font = .systemFont(ofSize: 16, weight: .semibold) label.textColor = ThemeManager.shared.color.titleAuxColor return label }() private(set) lazy var hotGroupsCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 75, height: 88) layout.minimumLineSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(HotGroupCell.self, forCellWithReuseIdentifier: HotGroupCell.reuseId) return cv }() // MARK: - Segment private(set) lazy var segmentView: UIView = { let v = UIView() v.backgroundColor = .white let line = UIView() line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12) v.addSubview(line) line.layoutChain .edgesHorzontal() .height(1) .bottom(4) return v }() private(set) lazy var createdTabLabel: UILabel = { let label = UILabel() label.text = "我创建的圈子" label.font = .systemFont(ofSize: 15, weight: .semibold) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() private(set) lazy var joinedTabLabel: UILabel = { let label = UILabel() label.text = "我加入的圈子" label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() private lazy var tabIndicator: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#16B3FF") v.layer.cornerRadius = 2 return v }() // MARK: - 横向滚动容器 (PanScrollView) private(set) lazy var segmentScrollView: PanScrollView = { let sv = PanScrollView() sv.isPagingEnabled = true sv.showsHorizontalScrollIndicator = false sv.bounces = false sv.delegate = self return sv }() private lazy var segmentContentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 两个列表 private(set) lazy var createdTableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.rowHeight = 72 tv.bounces = true tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) return tv }() private(set) lazy var joinedTableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.rowHeight = 72 tv.bounces = true tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) return tv }() // MARK: - Segment var currentSegmentIndex: Int = 0 func selectSegment(at index: Int) { currentSegmentIndex = index let isCreated = index == 0 UIView.animate(withDuration: 0.2) { self.createdTabLabel.textColor = isCreated ? UIColor(hexStr: "#1A1A1A") : UIColor(hexStr: "#999999") self.joinedTabLabel.textColor = isCreated ? UIColor(hexStr: "#999999") : UIColor(hexStr: "#1A1A1A") self.tabIndicator.center.x = isCreated ? self.createdTabLabel.center.x : self.joinedTabLabel.center.x self.layoutIfNeeded() } } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UIScrollViewDelegate(参考 PanView + PanSubItemView 通知模式) extension GroupView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == mainScrollView { handleMainScroll(scrollView) } else if scrollView == segmentScrollView { let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) if page != currentSegmentIndex, page >= 0, page < 2 { currentSegmentIndex = page selectSegment(at: page) } } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard scrollView != mainScrollView, scrollView != segmentScrollView else { return } if scrollView.contentOffset.y > 0 { isSubCanScroll = true } } /// 由 GroupViewController 通过 rx.didScroll 转发调用 func handleTableViewScroll(_ scrollView: UIScrollView) { if isSubCanScroll { if scrollView.contentOffset.y <= 0 { isSubCanScroll = false isSticky = false scrollView.contentOffset.y = 0 } } else { scrollView.contentOffset.y = 0 } } // MARK: - 主 scroll 逻辑 private func handleMainScroll(_ scrollView: UIScrollView) { if isSubCanScroll { scrollView.contentOffset.y = stickThreshold isSticky = true selectSegment(at: currentSegmentIndex) } else { let offsetY = scrollView.contentOffset.y if offsetY >= stickThreshold { scrollView.contentOffset.y = stickThreshold isSticky = true isSubCanScroll = true selectSegment(at: currentSegmentIndex) } } } } // MARK: - HotGroupCell final class HotGroupCell: UICollectionViewCell { static let reuseId = "HotGroupCell" private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 10 iv.clipsToBounds = true iv.backgroundColor = .clear return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = ThemeManager.shared.color.contentColor label.textAlignment = .center return label }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) iconView.layoutChain .top().centerX() .width(50).height(50) nameLabel.layoutChain .topToBottomOfView(iconView, offset: 6) .edgesHorzontal() countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 2) .edgesHorzontal() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ item: HotGroupItem) { iconView.image = UIImage(named: item.icon) nameLabel.text = item.name countLabel.text = "\(item.memberCount)人·\(item.onlineCount)在线" } } // MARK: - CircleGroupCell final class CircleGroupCell: UITableViewCell { private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 24 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#F0F0F0") return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#1A1A1A") return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor(hexStr: "#999999") return label }() private let addressLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor(hexStr: "#999999") return label }() private let dividerLine: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F0F0F0") return v }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .white contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) contentView.addSubview(addressLabel) contentView.addSubview(dividerLine) iconView.layoutChain .left(16).centerY() .width(48).height(48) nameLabel.layoutChain .top(12).leftToRightOfView(iconView, offset: 12) countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 4) .leftToView(nameLabel) addressLabel.layoutChain .centerY().right(16) dividerLine.layoutChain .left(76).right().bottom().height(0.5) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ item: CircleGroupItem) { iconView.image = UIImage(named: item.icon) nameLabel.text = item.name countLabel.text = "\(item.memberCount)人·\(item.onlineCount)在线" addressLabel.text = item.address } }