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

278 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 var searchHistoryViewController: 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)
self.searchHistoryViewController = SearchHistoryViewController()
searchHistoryViewController.context = appContext
searchHistoryViewController.coordinator = sceneCoordinator
searchHistoryViewController.viewModel = SearchHistoryViewModel(context: appContext, authContext: authContext)
super.init(nibName: nil, bundle: nil)
searchResultOverviewCoordinator.delegate = searchHistoryViewController
}
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
navigationItem.preferredSearchBarPlacement = .stacked
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)
}
}
}