XSeri/XSeri/Class/Home/Controller/XSSearchViewController.swift
2026-03-03 13:53:32 +08:00

391 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<Void, Never>?
private var resultTask: Task<Void, Never>?
private var hotTask: Task<Void, Never>?
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)
}
}