// // XSSearchViewController.swift // XSeri // // Created by 长沙鸿瑶 on 2026/2/2. // import UIKit import SnapKit import LYEmptyView final class XSSearchViewController: XSViewController { private enum Mode { case idle case suggest case result } private let headerView = XSSearchHeaderView() private let historyHotView = XSSearchHistoryHotView() private lazy var suggestionCollectionView: XSCollectionView = { let layout = UICollectionViewFlowLayout() layout.minimumLineSpacing = 12 layout.minimumInteritemSpacing = 0 layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) collectionView.isScrollEnabled = true collectionView.keyboardDismissMode = .onDrag collectionView.delegate = self collectionView.dataSource = self collectionView.register(XSSearchSuggestionCell.self, forCellWithReuseIdentifier: "XSSearchSuggestionCell") return collectionView }() private lazy var resultCollectionView: XSCollectionView = { let layout = UICollectionViewFlowLayout() layout.minimumLineSpacing = 16 layout.minimumInteritemSpacing = 8 layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) collectionView.isScrollEnabled = true collectionView.keyboardDismissMode = .onDrag collectionView.delegate = self collectionView.dataSource = self collectionView.register(XSSearchResultCell.self, forCellWithReuseIdentifier: "XSSearchResultCell") return collectionView }() private var history: [String] = [] private var suggestionItems: [XSShortModel] = [] private var resultItems: [XSShortModel] = [] private var currentSuggestKeyword = "" private var currentResultKeyword = "" private var mode: Mode = .idle { didSet { xs_updateVisibility() } } private var suggestionTask: Task? private var resultTask: Task? private var hotTask: Task? deinit { suggestionTask?.cancel() resultTask?.cancel() hotTask?.cancel() } override func viewDidLoad() { super.viewDidLoad() xs_setupUI() xs_bindData() xs_loadHistory() xs_loadHotSearches() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: true) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let _ = self.headerView.becomeFirstResponder() } } extension XSSearchViewController { private func xs_setupUI() { view.addSubview(headerView) view.addSubview(historyHotView) view.addSubview(suggestionCollectionView) view.addSubview(resultCollectionView) headerView.snp.makeConstraints { make in make.top.equalToSuperview().offset(XSScreen.safeTop + 12) make.left.equalToSuperview().offset(16) make.right.equalToSuperview().offset(-16) make.height.equalTo(38) } historyHotView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(22) make.left.right.equalToSuperview() } suggestionCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(22) make.left.right.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide) } resultCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(22) make.left.right.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide) } // 搜索结果为空时显示空白样式 resultCollectionView.ly_emptyView = XSEmpty.xs_emptyView() // 联想为空时显示空白样式 suggestionCollectionView.ly_emptyView = XSEmpty.xs_emptyView() } private func xs_bindData() { headerView.placeholder = XSSearchData.searchPlaceholder headerView.didTapBack = { [weak self] in self?.navigationController?.popViewController(animated: true) } headerView.didBeginEditing = { [weak self] text in guard let self = self else { return } // 已经显示搜索结果时,不在进入编辑时立刻切换联想 if self.mode == .result { return } self.xs_handleSuggest(text) } headerView.didChangeText = { [weak self] text in self?.xs_handleSuggest(text) } headerView.didTapSearch = { [weak self] text in self?.xs_submitSearch(text) } historyHotView.historyTitle = XSSearchData.recentTitle historyHotView.didTapClear = { [weak self] in self?.xs_clearHistory() } historyHotView.didSelectHistory = { [weak self] text in self?.headerView.text = text self?.xs_submitSearch(text) } historyHotView.didSelectHot = { [weak self] model in self?.xs_pushDetail(model: model) } historyHotView.hotTitle = XSSearchData.hotSearchesTitle xs_updateVisibility() } } // MARK: - Data extension XSSearchViewController { private func xs_loadHotSearches() { hotTask?.cancel() hotTask = Task { [weak self] in // 热门榜单数据来源接口 let hotList = await XSHomeAPI.requestHotSearchList() ?? [] let topList = await XSHomeAPI.requestTopSearchList() ?? [] guard !Task.isCancelled else { return } await MainActor.run { self?.xs_updateHotSection(hotList: Array(hotList.prefix(5)), topList: Array(topList.prefix(5))) } } } private func xs_updateHotSection(hotList: [XSShortModel], topList: [XSShortModel]) { var arr: [XSSearchHotSection] = [] if !hotList.isEmpty { arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_03"), title: XSSearchData.hotSectionTitles[0], style: .gold, items: hotList)) } if !topList.isEmpty { arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_04"), title: XSSearchData.hotSectionTitles[1], style: .purple, items: topList)) } historyHotView.hotSections = arr xs_updateVisibility() } private func xs_fetchSuggestions(_ keyword: String) { suggestionTask?.cancel() let expectedKeyword = keyword suggestionTask = Task { [weak self] in // 联想词同样走搜索接口 let list = await XSHomeAPI.requestSearch(text: keyword) ?? [] guard !Task.isCancelled else { return } await MainActor.run { guard let self = self, self.mode == .suggest, self.currentSuggestKeyword == expectedKeyword else { return } self.suggestionItems = list self.suggestionCollectionView.reloadData() } } } private func xs_fetchResults(_ keyword: String) { resultTask?.cancel() let expectedKeyword = keyword resultTask = Task { [weak self] in // 搜索结果走搜索接口 let list = await XSHomeAPI.requestSearch(text: keyword) ?? [] guard !Task.isCancelled else { return } await MainActor.run { guard let self = self, self.mode == .result, self.currentResultKeyword == expectedKeyword else { return } self.resultItems = list self.resultCollectionView.reloadData() } } } } // MARK: - Actions extension XSSearchViewController { private func xs_handleSuggest(_ text: String?) { let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if keyword.isEmpty { suggestionTask?.cancel() resultTask?.cancel() currentSuggestKeyword = "" mode = .idle suggestionItems = [] suggestionCollectionView.reloadData() return } resultTask?.cancel() resultItems = [] resultCollectionView.reloadData() resultCollectionView.isHidden = true currentSuggestKeyword = keyword mode = .suggest suggestionCollectionView.isHidden = false xs_fetchSuggestions(keyword) } private func xs_submitSearch(_ text: String?) { let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !keyword.isEmpty else { mode = .idle return } suggestionTask?.cancel() suggestionItems = [] suggestionCollectionView.reloadData() suggestionCollectionView.isHidden = true currentResultKeyword = keyword view.endEditing(true) mode = .result xs_addHistory(keyword) xs_fetchResults(keyword) } } // MARK: - History extension XSSearchViewController { private func xs_loadHistory() { history = UserDefaults.standard.stringArray(forKey: XSSearchData.historyKey) ?? [] historyHotView.historyTags = history xs_updateVisibility() } private func xs_addHistory(_ keyword: String) { // 搜索历史本地缓存,最多保留 10 条 history.removeAll { $0.caseInsensitiveCompare(keyword) == .orderedSame } history.insert(keyword, at: 0) if history.count > 10 { history = Array(history.prefix(10)) } UserDefaults.standard.set(history, forKey: XSSearchData.historyKey) historyHotView.historyTags = history xs_updateVisibility() } private func xs_clearHistory() { history = [] UserDefaults.standard.removeObject(forKey: XSSearchData.historyKey) historyHotView.historyTags = [] xs_updateVisibility() } } // MARK: - Layout extension XSSearchViewController { private func xs_updateVisibility() { switch mode { case .idle: historyHotView.isHidden = !historyHotView.hasContent suggestionCollectionView.isHidden = true resultCollectionView.isHidden = true headerView.style = .home case .suggest: historyHotView.isHidden = true suggestionCollectionView.isHidden = false resultCollectionView.isHidden = true headerView.style = .input case .result: historyHotView.isHidden = true suggestionCollectionView.isHidden = true resultCollectionView.isHidden = false headerView.style = .input } } } // MARK: - UICollectionViewDelegate UICollectionViewDataSource extension XSSearchViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if collectionView == suggestionCollectionView { return suggestionItems.count } return resultItems.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if collectionView == suggestionCollectionView { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchSuggestionCell", for: indexPath) as! XSSearchSuggestionCell let item = suggestionItems[indexPath.row] let keyword = headerView.text ?? "" cell.configure(model: item, keyword: keyword) return cell } else { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchResultCell", for: indexPath) as! XSSearchResultCell cell.configure(model: resultItems[indexPath.row]) return cell } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if collectionView == suggestionCollectionView { let item = suggestionItems[indexPath.row] xs_pushDetail(model: item) } else if collectionView == resultCollectionView { let item = resultItems[indexPath.row] xs_pushDetail(model: item) } } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { if collectionView == suggestionCollectionView { let width = XSScreen.width - 32 return CGSize(width: floor(width), height: 104) } else { let width = (XSScreen.width - 32 - 16) / 3 return CGSize(width: floor(width), height: 186) } } } // MARK: - Detail extension XSSearchViewController { private func xs_pushDetail(model: XSShortModel) { let shortId = model.short_play_id ?? model.short_play_video_id ?? model.id guard let shortId = shortId, !shortId.isEmpty else { return } let controller = XSShortDetailViewController() controller.shortId = shortId navigationController?.pushViewController(controller, animated: true) } }