LNUserSearchViewController.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. //
  2. // LNUserSearchViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/12.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. extension UIView {
  11. func pushToUserSearch() {
  12. let vc = LNUserSearchViewController()
  13. navigationController?.pushViewController(vc, animated: true)
  14. }
  15. }
  16. enum LNUserSearchTab: Int, CaseIterable {
  17. case overview
  18. case user
  19. case rooms
  20. var title: String {
  21. switch self {
  22. case .overview:
  23. .init(key: "A00363")
  24. case .user:
  25. .init(key: "A00364")
  26. case .rooms:
  27. .init(key: "A00365")
  28. }
  29. }
  30. }
  31. class LNUserSearchViewController: LNViewController {
  32. private let searchInput = UITextField()
  33. private let historyView = LNUserSearchHistoryView()
  34. private let searchView = UIView()
  35. private let scrollView = UIScrollView()
  36. private let tabsView = LNUserSearchTabsView()
  37. private let overviewListView = LNUserSearchOverviewListView()
  38. private let userListView = LNUserSearchUserListView()
  39. private let roomListView = LNUserSearchRoomListView()
  40. private lazy var resultViews = [overviewListView, userListView, roomListView]
  41. override func viewDidLoad() {
  42. super.viewDidLoad()
  43. setupViews()
  44. showLandingState()
  45. }
  46. }
  47. extension LNUserSearchViewController: UIScrollViewDelegate {
  48. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  49. syncTabFromPageScroll()
  50. }
  51. func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  52. syncTabFromPageScroll()
  53. }
  54. }
  55. extension LNUserSearchViewController: LNUserSearchHistoryViewDelegate {
  56. func onUserSearchHistoryView(view: LNUserSearchHistoryView, didClick history: String) {
  57. searchInput.text = history
  58. triggerSearchIfNeeded()
  59. }
  60. }
  61. extension LNUserSearchViewController: UITextFieldDelegate {
  62. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  63. triggerSearchIfNeeded()
  64. return true
  65. }
  66. }
  67. extension LNUserSearchViewController: LNUserSearchOverviewListViewDelegate {
  68. func onUserSearchOverviewListView(view: LNUserSearchOverviewListView, didClickMore tab: LNUserSearchTab) {
  69. selectTab(tab, animated: true)
  70. }
  71. }
  72. extension LNUserSearchViewController {
  73. private func triggerSearchIfNeeded() {
  74. searchInput.resignFirstResponder()
  75. let keyword = searchInput.text?
  76. .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
  77. if keyword.isEmpty {
  78. showLandingState()
  79. return
  80. }
  81. searchInput.text = keyword
  82. historyView.addRecord(keyword)
  83. overviewListView.search(keyword)
  84. userListView.search(keyword: keyword)
  85. roomListView.search(keyword: keyword)
  86. searchView.isHidden = false
  87. historyView.isHidden = true
  88. userListView.search(keyword: keyword)
  89. selectTab(.overview, animated: false)
  90. }
  91. private func showLandingState() {
  92. searchView.isHidden = true
  93. historyView.isHidden = false
  94. }
  95. private func selectTab(_ tab: LNUserSearchTab, animated: Bool) {
  96. tabsView.update(selected: tab, animated: animated)
  97. guard scrollView.bounds.width > 0 else {
  98. return
  99. }
  100. let offsetX: CGFloat = scrollView.bounds.width * CGFloat(tab.rawValue)
  101. scrollView.setContentOffset(.init(x: offsetX, y: 0), animated: animated)
  102. if !animated {
  103. syncTabFromPageScroll()
  104. }
  105. }
  106. private func syncTabFromPageScroll() {
  107. guard scrollView.bounds.width > 0 else { return }
  108. let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width))
  109. guard let tab = LNUserSearchTab(rawValue: max(0, min(page, LNUserSearchTab.allCases.count - 1))) else { return }
  110. tabsView.update(selected: tab, animated: true)
  111. }
  112. }
  113. extension LNUserSearchViewController {
  114. private func setupViews() {
  115. setupNavBar()
  116. historyView.delegate = self
  117. view.addSubview(historyView)
  118. historyView.snp.makeConstraints { make in
  119. make.horizontalEdges.equalToSuperview()
  120. make.top.equalToSuperview()
  121. }
  122. let resultView = buildResultView()
  123. view.addSubview(resultView)
  124. resultView.snp.makeConstraints { make in
  125. make.edges.equalToSuperview()
  126. }
  127. view.onTap { [weak self] in
  128. self?.view.endEditing(true)
  129. }
  130. }
  131. private func setupNavBar() {
  132. let search = UIButton()
  133. search.setTitle(.init(key: "A00245"), for: .normal)
  134. search.setTitleColor(.text_5, for: .normal)
  135. search.titleLabel?.font = .heading_h4
  136. search.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  137. search.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  138. search.addAction(UIAction(handler: { [weak self] _ in
  139. self?.triggerSearchIfNeeded()
  140. }), for: .touchUpInside)
  141. setRightButton(search)
  142. let input = buildSearchInput()
  143. setTitleView(input)
  144. }
  145. private func buildSearchInput() -> UIView {
  146. let container = UIView()
  147. container.backgroundColor = .fill_2
  148. container.layer.cornerRadius = 18
  149. container.snp.makeConstraints { make in
  150. make.height.equalTo(36)
  151. make.width.equalTo(view.bounds.width).priority(.medium)
  152. }
  153. let icon = UIImageView()
  154. icon.image = .icMagnifyingglass
  155. container.addSubview(icon)
  156. icon.snp.makeConstraints { make in
  157. make.leading.equalToSuperview().offset(10)
  158. make.centerY.equalToSuperview()
  159. make.width.height.equalTo(18)
  160. }
  161. searchInput.font = .body_s
  162. searchInput.textColor = .text_5
  163. searchInput.placeholder = .init(key: "A00246")
  164. searchInput.clearButtonMode = .always
  165. searchInput.returnKeyType = .search
  166. searchInput.enablesReturnKeyAutomatically = true
  167. searchInput.delegate = self
  168. searchInput.becomeFirstResponder()
  169. searchInput.addAction(UIAction(handler: { [weak self] _ in
  170. guard let self else { return }
  171. if searchInput.text?.isEmpty != false {
  172. showLandingState()
  173. }
  174. }), for: .editingChanged)
  175. container.addSubview(searchInput)
  176. searchInput.snp.makeConstraints { make in
  177. make.leading.equalTo(icon.snp.trailing).offset(8)
  178. make.centerY.equalToSuperview()
  179. make.trailing.equalToSuperview().offset(-10)
  180. }
  181. return container
  182. }
  183. private func buildResultView() -> UIView {
  184. tabsView.onSelect = { [weak self] tab in
  185. self?.selectTab(tab, animated: true)
  186. }
  187. searchView.addSubview(tabsView)
  188. tabsView.snp.makeConstraints { make in
  189. make.horizontalEdges.equalToSuperview()
  190. make.top.equalToSuperview()
  191. }
  192. scrollView.isPagingEnabled = true
  193. scrollView.delegate = self
  194. searchView.addSubview(scrollView)
  195. scrollView.snp.makeConstraints { make in
  196. make.horizontalEdges.equalToSuperview()
  197. make.bottom.equalToSuperview()
  198. make.top.equalTo(tabsView.snp.bottom)
  199. }
  200. let stackView = UIStackView()
  201. scrollView.addSubview(stackView)
  202. stackView.snp.makeConstraints { make in
  203. make.edges.equalToSuperview()
  204. make.height.equalToSuperview()
  205. }
  206. overviewListView.delegate = self
  207. for view in resultViews {
  208. stackView.addArrangedSubview(view)
  209. view.snp.makeConstraints { make in
  210. make.width.height.equalTo(scrollView)
  211. }
  212. }
  213. return searchView
  214. }
  215. }
  216. #if DEBUG
  217. import SwiftUI
  218. struct LNIMSearchViewControllerPreview: UIViewControllerRepresentable {
  219. func makeUIViewController(context: Context) -> some UIViewController {
  220. LNUserSearchViewController()
  221. }
  222. func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
  223. }
  224. }
  225. #Preview(body: {
  226. LNIMSearchViewControllerPreview()
  227. })
  228. #endif