AutoCat/AutoCat/Controllers/SearchController.swift

318 lines
13 KiB
Swift

import UIKit
import RxSwift
import RxCocoa
import RealmSwift
import PKHUD
import ExceptionCatcher
import AutoCatCore
class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDelegate, UIScrollViewDelegate {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var showMapButton: UIBarButtonItem?
private var refreshButton: UIBarButtonItem!
private var refreshIndicator: UIBarButtonItem!
private var moreActionsButton: UIBarButtonItem?
private let bag = DisposeBag()
private lazy var searchController: UISearchController = .default
.placeholder(NSLocalizedString("Search plate numbers", comment: ""))
.resultsUpdater(self)
.makeDumb()
private var refreshControl = UIRefreshControl()
private var datasource: RxSectionedDataSource<Vehicle,VehicleCell>!
private var isLoadingPage = false
private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium)
var filterRelay = BehaviorRelay<Filter>(value: Filter())
var filter = Filter()
override func viewDidLoad() {
super.viewDidLoad()
self.showMapButton?.isEnabled = false
navigationItem.searchController = searchController
definesPresentationContext = true
if #available(iOS 14.0, *) {
setupActionsMenu()
}
self.refreshButton = UIBarButtonItem(image: UIImage(systemName: "arrow.triangle.2.circlepath"),
style: .plain,
target: self,
action: #selector(refresh))
self.refreshIndicator = UIBarButtonItem(customView: self.pageLoadingIndicator)
#if targetEnvironment(macCatalyst)
self.navigationItem.leftBarButtonItem = self.refreshButton
#endif
//self.refreshControl.attributedTitle = NSAttributedString(string: "")
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
self.tableView.addSubview(self.refreshControl)
self.datasource = RxSectionedDataSource(table: self.tableView)
self.tableView.delegate = self
DispatchQueue.main.async {
self.filterRelay
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.do(onNext: { _ in
self.showProgress()
})
.flatMapLatest { filter in
if filter.needReset {
self.datasource.reset()
}
return Api.getVehicles(with: filter, pageToken: self.datasource.pageToken)
.do(onError: { print($0) })
.catchErrorJustReturn(PagedResponse<Vehicle>())
}
.observeOn(MainScheduler.instance)
.do(onNext: {
if let count = $0.count {
self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count)
self.showMapButton?.isEnabled = count > 0
}
self.refreshControl.endRefreshing()
self.isLoadingPage = false
self.pageLoadingIndicator.stopAnimating()
self.hideProgress()
})
.bind(to: self.datasource.data)
.disposed(by: self.bag)
}
}
func showProgress() {
navigationItem.leftBarButtonItem = self.refreshIndicator
pageLoadingIndicator.startAnimating()
moreActionsButton?.isEnabled = false
}
func hideProgress() {
#if targetEnvironment(macCatalyst)
navigationItem.leftBarButtonItem = self.refreshButton
#else
navigationItem.leftBarButtonItem = nil
#endif
moreActionsButton?.isEnabled = true
}
// FIXME: Code duplication
func updateDetailController(with vehicle: Vehicle) {
if let splitViewController = self.view.window?.rootViewController as? UISplitViewController
{
var detail: UINavigationController?
if splitViewController.viewControllers.count == 2 {
detail = splitViewController.viewControllers.last as? UINavigationController
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
detail = storyboard.instantiateViewController(identifier: "ReportNavController")
}
if let detail = detail {
detail.popToRootViewController(animated: true)
let report = detail.viewControllers.first as? ReportController
report?.vehicle = vehicle
splitViewController.showDetailViewController(detail, sender: self)
//self.performSegue(withIdentifier: "OpenDetailSegue", sender: self)
}
}
}
// MARK: - UISearchResultsUpdating
func updateSearchResults(for searchController: UISearchController) {
let newQuery = searchController.searchBar.text?.uppercased() ?? ""
guard self.filter.searchString != newQuery else { return }
self.filter.searchString = newQuery
self.filter.needReset = true
self.filterRelay.accept(self.filter)
}
// MARK: NavigationBar actions
@available(iOS 14.0, *)
func setupActionsMenu() {
let menu = UIMenu(children: [
UIAction(title: NSLocalizedString("Filter results", comment: ""),
image: UIImage(systemName: "line.horizontal.3.decrease"),
handler: { _ in self.showFilter() }),
UIAction(title: NSLocalizedString("Show on map", comment: ""),
image: UIImage(systemName: "map"),
handler: { _ in self.showOnMap() }),
UIAction(title: NSLocalizedString("Export", comment: ""),
image: UIImage(systemName: "square.and.arrow.up"),
handler: { _ in self.exportSearchResults() })
])
let menuBarButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "ellipsis"), primaryAction: nil, menu: menu)
self.navigationItem.rightBarButtonItems = [menuBarButton]
self.moreActionsButton = menuBarButton
}
@IBAction func onFilterTapped(_ sender: UIBarButtonItem) {
showFilter()
}
@IBAction func onMapTapped(_ sender: UIBarButtonItem) {
showOnMap()
}
@objc func refresh(_ sender: AnyObject) {
self.showMapButton?.isEnabled = false
self.filter.needReset = true
self.filterRelay.accept(self.filter)
}
func showFilter() {
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "FiltersController") as FiltersController
controller.filter = self.filter
controller.onDone = {
self.filter = controller.filter
self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate)
self.filter.needReset = true
self.filterRelay.accept(self.filter)
}
self.navigationController?.pushViewController(controller, animated: true)
}
func showOnMap() {
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "GlobalEventsNavigation") as UINavigationController
if let eventsVC = controller.viewControllers.first as? GlobalEventsController {
eventsVC.filter = self.filter
}
controller.modalPresentationStyle = .fullScreen
self.present(controller, animated: true)
}
func exportSearchResults() {
showProgress()
Api.getVehicles(with: filter, pageSize: 0)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { resp in
self.hideProgress()
let newLine = "\r\n"
var csvString = Vehicle.csvHeader + newLine
for vehicle in resp.items {
csvString.append(vehicle.csvLine)
csvString.append(newLine)
}
do {
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
#if targetEnvironment(macCatalyst)
self.save(file: tmpUrl)
#else
self.share(file: tmpUrl)
#endif
} catch {
HUD.show(error: error)
}
}, onError: { error in
self.hideProgress()
HUD.show(error: error)
})
.disposed(by: bag)
}
func share(file url: URL) {
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
self.present(activityController, animated: true)
}
func save(file url: URL) {
if #available(iOS 14, *) {
let controller = UIDocumentPickerViewController(forExporting: [url])
self.present(controller, animated: true)
} else {
let controller = UIDocumentPickerViewController(url: url, in: .exportToService)
present(controller, animated: true)
}
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let vehicle = self.datasource.item(at: indexPath)
let updateAction = UIContextualAction(style: .normal, title: NSLocalizedString("Update", comment: "")) { action, view, completion in
self.update(vehicle: vehicle, at: indexPath)
completion(true)
}
updateAction.image = UIImage(systemName: "arrow.2.circlepath")
updateAction.backgroundColor = .systemBlue
let configuration = UISwipeActionsConfiguration(actions: [updateAction])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let vehicle = self.datasource.item(at: indexPath)
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
let update = UIAction(title: NSLocalizedString("Update", comment: ""), image: UIImage(systemName: "arrow.2.circlepath")) { action in
self.update(vehicle: vehicle, at: indexPath)
}
return UIMenu(title: NSLocalizedString("Actions", comment: ""), children: [update])
}
}
func update(vehicle: Vehicle, at indexPath: IndexPath) {
HUD.show(.progress)
Api.checkVehicle(by: vehicle.getNumber(), notes: Array(vehicle.notes), events: [], force: true).observeOn(MainScheduler.instance).subscribe { newVehicle in
HUD.hide()
do {
let realm = try Realm()
if realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) != nil {
try realm.write {
realm.add(newVehicle, update: .all)
}
}
} catch {
print(error)
self.show(error: error)
}
let frozenVehicle = newVehicle.realm != nil ? newVehicle.clone() : newVehicle
self.datasource.set(item: frozenVehicle, at: indexPath)
self.updateDetailController(with: frozenVehicle)
} onError: { err in
HUD.show(error: err)
}.disposed(by: self.bag)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vehicle = self.datasource.item(at: indexPath)
self.updateDetailController(with: vehicle)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard tableView.contentSize.height > 0 else { return }
let toBottom = tableView.contentSize.height - (tableView.contentOffset.y + tableView.frame.size.height)
if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() {
self.isLoadingPage = true
self.filter.needReset = false
self.filterRelay.accept(self.filter)
}
}
}