Files
appchk-app/main/Recordings/TVCAppSearch.swift
2020-09-08 11:52:07 +02:00

191 lines
5.6 KiB
Swift

import UIKit
protocol TVCAppSearchDelegate {
func appSearch(didSelect bundleId: String, appName: String?, developer: String?)
}
class TVCAppSearch: UITableViewController, UISearchBarDelegate {
private var dataSource: [AppStoreSearch.Result] = []
private var dataSourceLocal: [AppBundleInfo] = []
private var isLoading: Bool = false
private var searchActive: Bool = false
var delegate: TVCAppSearchDelegate?
private var searchNo = 0
private var searchError: Bool = false
private var downloadQueue: [URLSessionDownloadTask] = []
@IBOutlet private var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
BundleIcon.initCache()
dataSourceLocal = AppDB?.appBundleList() ?? []
}
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(closeThis))]
}
@objc private func closeThis() {
searchBar.endEditing(true)
dismiss(animated: true)
}
private func showManualEntryAlert() {
let alert = AskAlert(title: "App Name",
text: "Be as descriptive as possible. Preferably use app bundle id if available. Alternatively use app name or a link to a public repository.",
buttonText: "Set") {
self.delegate?.appSearch(didSelect: "_manually", appName: $0.textFields?.first?.text, developer: nil)
self.closeThis()
}
alert.addTextField { $0.placeholder = "com.apple.notes" }
alert.presentIn(self)
}
// MARK: - Table View Data Source
override func numberOfSections(in _: UITableView) -> Int {
dataSourceLocal.count > 0 ? 2 : 1
}
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return max(1, dataSource.count) + (searchActive ? 1 : 0)
case 1: return dataSourceLocal.count
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "AppStore"
case 1: return "Found in other recordings"
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppStoreSearchCell")!
let bundleId: String
let altLoadUrl: String?
switch indexPath.section {
case 0:
guard dataSource.count > 0, indexPath.row < dataSource.count else {
if indexPath.row == 0 {
if searchError {
cell.textLabel?.text = "Error loading results"
} else if isLoading {
cell.textLabel?.text = "Loading …"
} else {
cell.textLabel?.text = "No results"
}
cell.isUserInteractionEnabled = false
} else {
cell.textLabel?.text = "Create manually …"
}
cell.detailTextLabel?.text = nil
cell.imageView?.image = nil
return cell
}
let src = dataSource[indexPath.row]
bundleId = src.bundleId
altLoadUrl = src.imageURL
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.developer
case 1:
let src = dataSourceLocal[indexPath.row]
bundleId = src.bundleId
altLoadUrl = nil
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.author
default:
preconditionFailure()
}
let sno = searchNo
cell.imageView?.image = BundleIcon.image(bundleId) {
guard let u = altLoadUrl, let url = URL(string: u) else { return }
self.downloadQueue.append(BundleIcon.download(bundleId, url: url) {
DispatchQueue.main.async {
// make sure its the same request
guard sno == self.searchNo else { return }
tableView.reloadRows(at: [indexPath], with: .automatic)
}
})
}
cell.isUserInteractionEnabled = true
cell.imageView?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0:
guard indexPath.row < dataSource.count else {
showManualEntryAlert()
return
}
let src = dataSource[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.developer)
case 1:
let src = dataSourceLocal[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.author)
default: preconditionFailure()
}
closeThis()
}
// MARK: - Search Bar Delegate
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
isLoading = true
tableView.reloadData()
for x in downloadQueue { x.cancel() }
downloadQueue = []
if searchText.count > 0 {
perform(#selector(performSearch), with: nil, afterDelay: 0.4)
} else {
performSearch()
}
}
/// Internal callback function for delayed text evaluation.
/// This way we can avoid unnecessary searches while user is typing.
@objc private func performSearch() {
func setSource(_ newSource: [AppStoreSearch.Result], _ err: Bool) {
searchNo += 1
searchError = err
dataSource = searchActive ? newSource : []
tableView.reloadData()
}
isLoading = false
let term = searchBar.text?.lowercased() ?? ""
searchActive = term.count > 0
guard searchActive else {
setSource([], false)
return
}
AppStoreSearch.search(term) { source, error in
DispatchQueue.main.async {
setSource(source ?? [], error != nil)
}
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
closeThis()
}
}