// // LNUserSearchViewController.swift // Lanu // // Created by OneeChan on 2025/12/12. // import Foundation import UIKit import SnapKit import MJRefresh extension UIView { func pushToUserSearch() { let vc = LNUserSearchViewController() navigationController?.pushViewController(vc, animated: true) } } class LNUserSearchViewController: LNViewController { private let searchInput = UITextField() private let historyView = LNUserSearchHistoryView() private let emptyView = LNNoMoreDataView() private let tableView = UITableView() private var curKeyword: String? private var nextTag: String? private var curList: [LNGameMateSearchResultVO] = [] override func viewDidLoad() { super.viewDidLoad() setupViews() } } extension LNUserSearchViewController { private func searchUser() { guard let curKeyword, !curKeyword.isEmpty else { tableView.mj_header?.endRefreshing() tableView.mj_footer?.endRefreshingWithNoMoreData() return } view.endEditing(true) historyView.isHidden = true historyView.addRecord(curKeyword) tableView.isHidden = false LNGameMateManager.shared.searchGameMate(keyword: curKeyword, next: nextTag ?? "") { [weak self] list, next in guard let self else { return } guard let list else { tableView.mj_header?.endRefreshing() tableView.mj_footer?.endRefreshingWithNoMoreData() if curList.isEmpty { emptyView.showNetworkError() } return } if nextTag?.isEmpty != false { curList = list } else { curList.append(contentsOf: list) } nextTag = next tableView.reloadData() if curList.isEmpty { emptyView.showNoData(tips: .init(key: "A00244")) } else { emptyView.hide() } self.tableView.mj_header?.endRefreshing() if next?.isEmpty != false { tableView.mj_footer?.endRefreshingWithNoMoreData() } else { tableView.mj_footer?.endRefreshing() } } } func reportExposure() { guard tableView.contentOffset.y >= 0 else { return } guard let indexs = tableView.indexPathsForVisibleRows, !indexs.isEmpty else { return } var items: [LNGameMateSearchResultVO] = [] for index in indexs { items.append(curList[index.row]) } LNStatisticManager.shared.reportExposure(uids: items.map({ $0.userNo })) { _ in } } } extension LNUserSearchViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { curList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: LNUserSearchItemCell.className, for: indexPath) as! LNUserSearchItemCell let item = curList[indexPath.row] cell.update(item) return cell } func scrollViewDidScroll(_ scrollView: UIScrollView) { if searchInput.isFirstResponder { view.endEditing(true) } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { reportExposure() } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { reportExposure() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { reportExposure() } } } extension LNUserSearchViewController: LNUserSearchHistoryViewDelegate { func onUserSearchHistoryView(view: LNUserSearchHistoryView, didClick history: String) { searchInput.text = history curKeyword = history nextTag = nil curList.removeAll() tableView.reloadData() tableView.mj_header?.beginRefreshing() } } extension LNUserSearchViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() guard let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return true } curKeyword = text nextTag = nil curList.removeAll() tableView.reloadData() tableView.mj_header?.beginRefreshing() return true } } extension LNUserSearchViewController { private func setupViews() { setupNavBar() let history = buildHistory() view.addSubview(history) history.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.top.equalToSuperview().offset(12) } let list = buildList() view.addSubview(list) list.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.top.equalToSuperview().offset(12) make.bottom.equalToSuperview() } view.onTap { [weak self] in guard let self else { return } view.endEditing(true) } } private func setupNavBar() { let search = UIButton() search.setTitle(.init(key: "A00245"), for: .normal) search.setTitleColor(.text_5, for: .normal) search.titleLabel?.font = .heading_h4 search.setContentHuggingPriority(.defaultHigh, for: .horizontal) search.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) search.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } _ = textFieldShouldReturn(searchInput) }), for: .touchUpInside) setRightButton(search) let input = buildSearchInput() setTitleView(input) } private func buildSearchInput() -> UIView { let container = UIView() container.backgroundColor = .fill_2 container.layer.cornerRadius = 18 container.snp.makeConstraints { make in make.height.equalTo(36) make.width.equalTo(view.bounds.width).priority(.medium) } let ic = UIImageView() ic.image = .icMagnifyingglass container.addSubview(ic) ic.snp.makeConstraints { make in make.leading.equalToSuperview().offset(10) make.centerY.equalToSuperview() make.width.height.equalTo(18) } searchInput.font = .body_s searchInput.placeholder = .init(key: "A00246") searchInput.clearButtonMode = .always searchInput.returnKeyType = .search searchInput.enablesReturnKeyAutomatically = true searchInput.delegate = self searchInput.becomeFirstResponder() searchInput.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } if searchInput.text?.isEmpty != false { historyView.isHidden = false tableView.isHidden = true } }), for: .editingChanged) container.addSubview(searchInput) searchInput.snp.makeConstraints { make in make.leading.equalTo(ic.snp.trailing).offset(8) make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-10) } return container } private func buildList() -> UIView { let header = MJRefreshNormalHeader { [weak self] in guard let self else { return } nextTag = nil searchUser() } 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 } searchUser() } 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.isHidden = true tableView.dataSource = self tableView.delegate = self tableView.separatorStyle = .none tableView.showsVerticalScrollIndicator = false tableView.showsHorizontalScrollIndicator = false tableView.register( LNUserSearchItemCell.self, forCellReuseIdentifier: LNUserSearchItemCell.className ) tableView.addSubview(emptyView) emptyView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.centerY.equalToSuperview().multipliedBy(0.6) } return tableView } private func buildHistory() -> UIView { historyView.isHidden = false historyView.delegate = self return historyView } } #if DEBUG import SwiftUI struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { LNUserSearchViewController() } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } } #Preview(body: { LNIMSearchViewControllerPreview() }) #endif