// // LNUserSearchViewController.swift // Lanu // // Created by OneeChan on 2025/12/12. // import Foundation import UIKit import SnapKit extension UIView { func pushToUserSearch() { let vc = LNUserSearchViewController() navigationController?.pushViewController(vc, animated: true) } } enum LNUserSearchTab: Int, CaseIterable { case overview case user case rooms var title: String { switch self { case .overview: .init(key: "A00363") case .user: .init(key: "A00364") case .rooms: .init(key: "A00365") } } } class LNUserSearchViewController: LNViewController { private let searchInput = UITextField() private let historyView = LNUserSearchHistoryView() private let searchView = UIView() private let scrollView = UIScrollView() private let tabsView = LNUserSearchTabsView() private let overviewListView = LNUserSearchOverviewListView() private let userListView = LNUserSearchUserListView() private let roomListView = LNUserSearchRoomListView() private lazy var resultViews = [overviewListView, userListView, roomListView] override func viewDidLoad() { super.viewDidLoad() setupViews() showLandingState() } } extension LNUserSearchViewController: UIScrollViewDelegate { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { syncTabFromPageScroll() } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { syncTabFromPageScroll() } } extension LNUserSearchViewController: LNUserSearchHistoryViewDelegate { func onUserSearchHistoryView(view: LNUserSearchHistoryView, didClick history: String) { searchInput.text = history triggerSearchIfNeeded() } } extension LNUserSearchViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { triggerSearchIfNeeded() return true } } extension LNUserSearchViewController: LNUserSearchOverviewListViewDelegate { func onUserSearchOverviewListView(view: LNUserSearchOverviewListView, didClickMore tab: LNUserSearchTab) { selectTab(tab, animated: true) } } extension LNUserSearchViewController { private func triggerSearchIfNeeded() { searchInput.resignFirstResponder() let keyword = searchInput.text? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if keyword.isEmpty { showLandingState() return } searchInput.text = keyword historyView.addRecord(keyword) overviewListView.search(keyword) userListView.search(keyword: keyword) roomListView.search(keyword: keyword) searchView.isHidden = false historyView.isHidden = true userListView.search(keyword: keyword) selectTab(.overview, animated: false) } private func showLandingState() { searchView.isHidden = true historyView.isHidden = false } private func selectTab(_ tab: LNUserSearchTab, animated: Bool) { tabsView.update(selected: tab, animated: animated) guard scrollView.bounds.width > 0 else { return } let offsetX: CGFloat = scrollView.bounds.width * CGFloat(tab.rawValue) scrollView.setContentOffset(.init(x: offsetX, y: 0), animated: animated) if !animated { syncTabFromPageScroll() } } private func syncTabFromPageScroll() { guard scrollView.bounds.width > 0 else { return } let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) guard let tab = LNUserSearchTab(rawValue: max(0, min(page, LNUserSearchTab.allCases.count - 1))) else { return } tabsView.update(selected: tab, animated: true) } } extension LNUserSearchViewController { private func setupViews() { setupNavBar() historyView.delegate = self view.addSubview(historyView) historyView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() } let resultView = buildResultView() view.addSubview(resultView) resultView.snp.makeConstraints { make in make.edges.equalToSuperview() } view.onTap { [weak self] in self?.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 self?.triggerSearchIfNeeded() }), 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 icon = UIImageView() icon.image = .icMagnifyingglass container.addSubview(icon) icon.snp.makeConstraints { make in make.leading.equalToSuperview().offset(10) make.centerY.equalToSuperview() make.width.height.equalTo(18) } searchInput.font = .body_s searchInput.textColor = .text_5 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 { showLandingState() } }), for: .editingChanged) container.addSubview(searchInput) searchInput.snp.makeConstraints { make in make.leading.equalTo(icon.snp.trailing).offset(8) make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-10) } return container } private func buildResultView() -> UIView { tabsView.onSelect = { [weak self] tab in self?.selectTab(tab, animated: true) } searchView.addSubview(tabsView) tabsView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() } scrollView.isPagingEnabled = true scrollView.delegate = self searchView.addSubview(scrollView) scrollView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.bottom.equalToSuperview() make.top.equalTo(tabsView.snp.bottom) } let stackView = UIStackView() scrollView.addSubview(stackView) stackView.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalToSuperview() } overviewListView.delegate = self for view in resultViews { stackView.addArrangedSubview(view) view.snp.makeConstraints { make in make.width.height.equalTo(scrollView) } } return searchView } } #if DEBUG import SwiftUI struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { LNUserSearchViewController() } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } } #Preview(body: { LNIMSearchViewControllerPreview() }) #endif