277 lines
11 KiB
Swift
277 lines
11 KiB
Swift
//
|
|
// SearchDetailViewController.swift
|
|
// Mastodon
|
|
//
|
|
// Created by MainasuK Cirno on 2021-7-13.
|
|
//
|
|
|
|
import UIKit
|
|
import Combine
|
|
import MastodonAsset
|
|
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 }
|
|
|
|
}
|
|
|
|
// Fake search bar not works on iPad with UISplitViewController
|
|
// check device and fallback to standard UISearchController
|
|
final class SearchDetailViewController: UIViewController, NeedsDependency {
|
|
|
|
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) } }
|
|
|
|
let isPhoneDevice: Bool = {
|
|
return UIDevice.current.userInterfaceIdiom == .phone
|
|
}()
|
|
|
|
var viewModel: SearchDetailViewModel!
|
|
|
|
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
|
|
|
|
let navigationBar = UINavigationBar(
|
|
frame: CGRect(x: 0, y: 0, width: 300, height: 100)
|
|
)
|
|
navigationBar.setItems([navigationItem], animated: false)
|
|
return navigationBar
|
|
}()
|
|
|
|
let searchController: CustomSearchController = {
|
|
let searchController = CustomSearchController()
|
|
searchController.automaticallyShowsScopeBar = false
|
|
searchController.obscuresBackgroundDuringPresentation = false
|
|
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
|
|
searchBar.sizeToFit()
|
|
return searchBar
|
|
}()
|
|
|
|
private(set) lazy var searchHistoryViewController: SearchHistoryViewController = {
|
|
let searchHistoryViewController = SearchHistoryViewController()
|
|
searchHistoryViewController.context = context
|
|
searchHistoryViewController.coordinator = coordinator
|
|
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context, authContext: viewModel.authContext)
|
|
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()
|
|
|
|
setupSearchBar()
|
|
|
|
addChild(searchHistoryViewController)
|
|
searchHistoryViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(searchHistoryViewController.view)
|
|
searchHistoryViewController.didMove(toParent: self)
|
|
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()
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
// bind search history display
|
|
viewModel.searchText
|
|
.removeDuplicates()
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] searchText in
|
|
guard let self = self else { return }
|
|
|
|
self.searchHistoryViewController.view.isHidden = !searchText.isEmpty
|
|
self.searchResultsOverviewViewController.view.isHidden = searchText.isEmpty
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
if isPhoneDevice {
|
|
searchBar.setShowsCancelButton(true, animated: animated)
|
|
UIView.performWithoutAnimation {
|
|
self.searchBar.becomeFirstResponder()
|
|
}
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SearchDetailViewController {
|
|
private func setupSearchBar() {
|
|
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)
|
|
} else {
|
|
navigationItem.setHidesBackButton(true, animated: false)
|
|
navigationItem.titleView = nil
|
|
navigationItem.searchController = searchController
|
|
searchController.searchBar.sizeToFit()
|
|
}
|
|
|
|
searchBar.delegate = self
|
|
}
|
|
|
|
private func setupBackgroundColor() {
|
|
navigationBarBackgroundView.backgroundColor = SystemTheme.navigationBarBackgroundColor
|
|
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)
|
|
}
|
|
|
|
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
|
guard let searchText = searchBar.text, searchText.isNotEmpty else { return }
|
|
|
|
searchBar.resignFirstResponder()
|
|
|
|
let searchResultViewModel = SearchResultViewModel(context: context, authContext: viewModel.authContext, searchScope: .all, searchText: searchText)
|
|
coordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show)
|
|
}
|
|
|
|
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
|
// dismiss or pop
|
|
if isModal {
|
|
dismiss(animated: true, completion: nil)
|
|
} else {
|
|
navigationController?.popViewController(animated: false)
|
|
}
|
|
}
|
|
}
|