// // LNGameMateListView.swift // Lanu // // Created by OneeChan on 2025/11/14. // import Foundation import UIKit import SnapKit import Combine import MJRefresh class LNGameMateListView: UIView { private(set) var curMateList: [LNGameMateListItemVO] = [] private var topCategory: String = "" private var category: String? private var filter = LNGameMateFilter() private var nextTag: String? private let pageSize = 30 private let emptyView = LNNoMoreDataView() private let tableView = UITableView() private var loading = false override init(frame: CGRect) { super.init(frame: frame) setupViews() } func loadListIfNeed() { if !curMateList.isEmpty { return } tableView.mj_header?.beginRefreshing() } func reloadList(newTopCategory: String, newCategory: String?, filter: LNGameMateFilter) { topCategory = newTopCategory category = newCategory self.filter = filter tableView.mj_header?.beginRefreshing() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNGameMateListView { private func loadList() { guard !loading else { return } loading = true let curNext = nextTag ?? "" LNGameMateManager.shared.getGameMateList( topCategory: topCategory, category: category, filter: filter, size: pageSize, next: curNext ) { [weak self] list, nextTag in guard let self else { return } loading = false if let list { if curNext.isEmpty { curMateList.removeAll() } curMateList.append(contentsOf: list) self.nextTag = nextTag tableView.reloadData() if curMateList.isEmpty { emptyView.showNoData(tips: .init(key: "A00039")) } else { emptyView.hide() } } else { if curMateList.isEmpty { emptyView.showNetworkError() } } self.tableView.mj_header?.endRefreshing() if nextTag?.isEmpty != false { tableView.mj_footer?.endRefreshingWithNoMoreData() } else { tableView.mj_footer?.endRefreshing() } } } func reportExposure() { let frame = convert(bounds, to: nil) guard frame.origin.x == 0, tableView.contentOffset.y >= 0 else { return } guard let indexs = tableView.indexPathsForVisibleRows, !indexs.isEmpty else { return } var items = Set() for index in indexs { items.insert(curMateList[index.row].userNo) } LNStatisticManager.shared.reportExposure(uids: Array(items)) { _ in } } } extension LNGameMateListView: LNGameFilterPanelDelegate { func onGameFilterPanel(panel: LNGameFilterPanel, didSelectFilter filter: LNGameMateFilter, topType: LNGameTypeItemVO, category: LNGameCategoryItemVO) { if viewController is LNHomeViewController { pushToGameMateList(topCategory: topType, category: category, filter: filter) } else { reloadList(newTopCategory: topType.code, newCategory: category.code, filter: filter) viewController?.title = category.name } } } extension LNGameMateListView: LNGameMateListMenuViewDelegate { func menuView(view: LNGameMateListMenuView, scoreTypeChanged newType: LNSortedType) { filter.sortByStar = newType tableView.mj_header?.beginRefreshing() } func menuView(view: LNGameMateListMenuView, priceTypeChanged newType: LNSortedType) { filter.sortByPrice = newType tableView.mj_header?.beginRefreshing() } func menuViewDidClickFind(view: LNGameMateListMenuView) { let filterPanel = LNGameFilterPanel() filterPanel.delegate = self filterPanel.popup() } } extension LNGameMateListView: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { curMateList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: LNGameMateListCell.className, for: indexPath) as! LNGameMateListCell let data = curMateList[indexPath.row] cell.update(data) return cell } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { reportExposure() } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { reportExposure() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { reportExposure() } } } extension LNGameMateListView { private func setupViews() { let menu = buildMenu() addSubview(menu) menu.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) make.top.equalToSuperview() } let listView = buildListView() addSubview(listView) listView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() make.top.equalTo(menu.snp.bottom).offset(10) } } private func buildMenu() -> UIView { let menu = LNGameMateListMenuView() menu.delegate = self return menu } private func buildListView() -> UIView { let header = MJRefreshNormalHeader { [weak self] in guard let self else { return } self.nextTag = nil self.loadList() } header.lastUpdatedTimeLabel?.isHidden = true header.stateLabel?.isHidden = true header.endRefreshingCompletionBlock = { [weak self] in guard let self else { return } reportExposure() } tableView.mj_header = header let footer = MJRefreshAutoNormalFooter { [weak self] in guard let self else { return } self.loadList() } footer.setTitle("", for: .noMoreData) footer.setTitle(.init(key: "A00046"), for: .idle) footer.endRefreshingCompletionBlock = { [weak self] in guard let self else { return } reportExposure() } tableView.mj_footer = footer tableView.register(LNGameMateListCell.self, forCellReuseIdentifier: LNGameMateListCell.className) tableView.dataSource = self tableView.delegate = self tableView.allowsSelection = false tableView.separatorStyle = .none tableView.backgroundColor = .clear tableView.addSubview(emptyView) emptyView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.centerY.equalToSuperview().multipliedBy(0.6) } return tableView } } #if DEBUG import SwiftUI struct LNGameMateListViewPreview: UIViewRepresentable { func makeUIView(context: Context) -> some UIView { let container = UIView() container.backgroundColor = .lightGray let view = LNGameMateListView() container.addSubview(view) view.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() } return container } func updateUIView(_ uiView: UIViewType, context: Context) { } } #Preview(body: { LNGameMateListViewPreview() }) #endif