mastodon-ios/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController....

284 lines
12 KiB
Swift
Raw Normal View History

//
// SearchDetailViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-13.
//
import UIKit
import Combine
import MastodonAsset
2022-10-08 07:43:06 +02:00
import MastodonCore
import MastodonLocalization
final class CustomSearchController: UISearchController {
let customSearchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
override var searchBar: UISearchBar { customSearchBar }
}
2021-09-27 09:28:26 +02:00
// Fake search bar not works on iPad with UISplitViewController
// check device and fallback to standard UISearchController
final class SearchDetailViewController: UIViewController, NeedsDependency {
2023-09-16 18:39:33 +02:00
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
let searchResultOverviewCoordinator: SearchResultOverviewCoordinator
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
2023-09-16 18:39:33 +02:00
2021-09-27 09:28:26 +02:00
let isPhoneDevice: Bool = {
return UIDevice.current.userInterfaceIdiom == .phone
}()
var viewModel: SearchDetailViewModel!
2021-07-15 14:28:36 +02:00
let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
let navigationBarBackgroundView = UIView()
let navigationBar: UINavigationBar = {
let navigationItem = UINavigationItem()
let barAppearance = UINavigationBarAppearance()
barAppearance.configureWithTransparentBackground()
navigationItem.standardAppearance = barAppearance
navigationItem.compactAppearance = barAppearance
navigationItem.scrollEdgeAppearance = barAppearance
2021-07-15 14:28:36 +02:00
let navigationBar = UINavigationBar(
frame: CGRect(x: 0, y: 0, width: 300, height: 100)
)
navigationBar.setItems([navigationItem], animated: false)
return navigationBar
}()
2021-09-27 09:28:26 +02:00
let searchController: CustomSearchController = {
let searchController = CustomSearchController()
2021-09-27 09:28:26 +02:00
searchController.automaticallyShowsScopeBar = false
2022-11-14 16:36:50 +01:00
searchController.obscuresBackgroundDuringPresentation = false
2021-09-27 09:28:26 +02:00
return searchController
}()
private(set) lazy var searchBar: UISearchBar = {
let searchBar: UISearchBar
if isPhoneDevice {
searchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
} else {
searchBar = searchController.searchBar
searchController.automaticallyShowsScopeBar = false
searchController.searchBar.setShowsScope(true, animated: false)
}
searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder
2021-07-15 14:28:36 +02:00
searchBar.sizeToFit()
return searchBar
}()
2021-07-15 14:28:36 +02:00
private(set) lazy var searchHistoryViewController: SearchHistoryViewController = {
let searchHistoryViewController = SearchHistoryViewController()
searchHistoryViewController.context = context
searchHistoryViewController.coordinator = coordinator
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
2021-07-15 14:28:36 +02:00
return searchHistoryViewController
}()
private(set) lazy var searchResultsOverviewViewController: SearchResultsOverviewTableViewController = {
return searchResultOverviewCoordinator.overviewViewController
}()
//MARK: - init
init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext) {
self.context = appContext
self.coordinator = sceneCoordinator
self.searchResultOverviewCoordinator = SearchResultOverviewCoordinator(appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
//MARK: - UIViewController
override func viewDidLoad() {
searchResultOverviewCoordinator.start()
super.viewDidLoad()
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
setupSearchBar()
2021-09-27 09:28:26 +02:00
2021-07-15 14:28:36 +02:00
addChild(searchHistoryViewController)
searchHistoryViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchHistoryViewController.view)
searchHistoryViewController.didMove(toParent: self)
2021-09-27 09:28:26 +02:00
if isPhoneDevice {
NSLayoutConstraint.activate([
searchHistoryViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
searchHistoryViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
searchHistoryViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
searchHistoryViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
} else {
searchHistoryViewController.view.pinToParent()
2021-09-27 09:28:26 +02:00
}
2021-07-15 14:28:36 +02:00
addChild(searchResultsOverviewViewController)
searchResultsOverviewViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchResultsOverviewViewController.view)
searchResultsOverviewViewController.didMove(toParent: self)
if isPhoneDevice {
NSLayoutConstraint.activate([
searchResultsOverviewViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor),
searchResultsOverviewViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
searchResultsOverviewViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
searchResultsOverviewViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
} else {
searchResultsOverviewViewController.view.pinToParent()
}
2021-07-15 14:28:36 +02:00
// bind search history display
viewModel.searchText
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] searchText in
guard let self = self else { return }
2021-07-15 14:28:36 +02:00
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
self.searchResultsOverviewViewController.view.isHidden = searchText.isEmpty
2021-07-15 14:28:36 +02:00
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
2021-09-27 09:28:26 +02:00
if isPhoneDevice {
navigationController?.setNavigationBarHidden(true, animated: animated)
searchBar.setShowsScope(true, animated: false)
searchBar.setNeedsLayout()
searchBar.layoutIfNeeded()
} else {
// do nothing
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
2021-09-27 09:28:26 +02:00
if isPhoneDevice {
if !isModal {
// prevent bar restore conflict with modal style issue
navigationController?.setNavigationBarHidden(false, animated: animated)
}
} else {
// do nothing
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
2021-09-27 09:28:26 +02:00
if isPhoneDevice {
searchBar.setShowsCancelButton(true, animated: animated)
UIView.performWithoutAnimation {
self.searchBar.becomeFirstResponder()
}
2021-09-27 09:28:26 +02:00
} else {
searchController.searchBar.setShowsCancelButton(true, animated: false)
searchController.searchBar.setShowsScope(true, animated: false)
UIView.performWithoutAnimation {
self.searchController.isActive = true
}
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
2021-09-27 09:28:26 +02:00
}
}
}
extension SearchDetailViewController {
private func setupSearchBar() {
2021-09-27 09:28:26 +02:00
if isPhoneDevice {
navigationBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationBar)
NSLayoutConstraint.activate([
navigationBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
navigationBar.topItem?.titleView = searchBar
navigationBar.layer.observe(\.bounds, options: [.new]) { [weak self] navigationBar, _ in
guard let self = self else { return }
self.viewModel.navigationBarFrame.value = navigationBar.frame
}
.store(in: &observations)
navigationBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(navigationBarBackgroundView, belowSubview: navigationBar)
NSLayoutConstraint.activate([
navigationBarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
navigationBarBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationBarBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
navigationBarBackgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
])
navigationBarVisualEffectBackgroundView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(navigationBarVisualEffectBackgroundView, belowSubview: navigationBarBackgroundView)
navigationBarVisualEffectBackgroundView.pinTo(to: navigationBarBackgroundView)
2021-09-27 09:28:26 +02:00
} else {
navigationItem.setHidesBackButton(true, animated: false)
navigationItem.titleView = nil
navigationItem.searchController = searchController
searchController.searchBar.sizeToFit()
}
searchBar.delegate = self
}
private func setupBackgroundColor(theme: Theme) {
navigationBarBackgroundView.backgroundColor = theme.navigationBarBackgroundColor
2023-06-02 09:52:12 +02:00
navigationBar.tintColor = Asset.Colors.Brand.blurple.color
}
}
// MARK: - UISearchBarDelegate
extension SearchDetailViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
viewModel.searchText.value = trimmedSearchText
searchResultsOverviewViewController.showStandardSearch(for: trimmedSearchText)
searchResultsOverviewViewController.searchForSuggestions(for: trimmedSearchText)
}
2023-09-17 15:16:47 +02:00
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text, searchText.isNotEmpty else { return }
2023-09-17 15:16:47 +02:00
searchBar.resignFirstResponder()
let searchResultViewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: .all, searchText: searchText)
coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show)
2023-09-17 15:16:47 +02:00
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
// dismiss or pop
if isModal {
dismiss(animated: true, completion: nil)
} else {
navigationController?.popViewController(animated: false)
}
}
}