diff --git a/main/Extensions/URL.swift b/main/Extensions/URL.swift index 6e2a0aa..8c41fc7 100644 --- a/main/Extensions/URL.swift +++ b/main/Extensions/URL.swift @@ -40,8 +40,8 @@ extension URL { return components.url } - func download(to file: URL, onSuccess: @escaping () -> Void) { - URLSession.shared.downloadTask(with: self) { location, response, error in + @discardableResult func download(to file: URL, onSuccess: @escaping () -> Void) -> URLSessionDownloadTask { + let task = URLSession.shared.downloadTask(with: self) { location, response, error in if let loc = location { try? FileManager.default.removeItem(at: file) do { @@ -51,6 +51,8 @@ extension URL { NSLog("[VPN.ERROR] \(error)") } } - }.resume() + } + task.resume() + return task } } diff --git a/main/Recordings/App Icons/AppStoreSearch.swift b/main/Recordings/App Icons/AppStoreSearch.swift index d16190a..ff814d3 100644 --- a/main/Recordings/App Icons/AppStoreSearch.swift +++ b/main/Recordings/App Icons/AppStoreSearch.swift @@ -20,15 +20,15 @@ struct AppStoreSearch { let developer, imageURL: String? } - static func search(_ term: String, _ closure: @escaping ([Result]?) -> Void) { + static func search(_ term: String, _ closure: @escaping ([Result]?, Error?) -> Void) { URLSession.shared.dataTask(with: .init(url: .appStoreSearch(query: term))) { data, response, error in guard let data = data, error == nil, let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else { - closure(nil) + closure(nil, error ?? URLError(.badServerResponse)) return } - closure(jsonSearchToList(data)) + closure(jsonSearchToList(data), nil) }.resume() } diff --git a/main/Recordings/App Icons/BundleIcon.swift b/main/Recordings/App Icons/BundleIcon.swift index 21951cd..07fe2a8 100644 --- a/main/Recordings/App Icons/BundleIcon.swift +++ b/main/Recordings/App Icons/BundleIcon.swift @@ -98,9 +98,7 @@ struct BundleIcon { return img } - static func download(_ bundleId: String, urlStr: String, whenDone: @escaping () -> Void) { - if let url = URL(string: urlStr) { - url.download(to: local(bundleId), onSuccess: whenDone) - } + static func download(_ bundleId: String, url: URL, whenDone: @escaping () -> Void) -> URLSessionDownloadTask { + return url.download(to: local(bundleId), onSuccess: whenDone) } } diff --git a/main/Recordings/TVCAppSearch.swift b/main/Recordings/TVCAppSearch.swift index abe3dcf..d266ea2 100644 --- a/main/Recordings/TVCAppSearch.swift +++ b/main/Recordings/TVCAppSearch.swift @@ -12,6 +12,10 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { 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() { @@ -29,6 +33,18 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { 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 { @@ -60,7 +76,13 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { case 0: guard dataSource.count > 0, indexPath.row < dataSource.count else { if indexPath.row == 0 { - cell.textLabel?.text = isLoading ? "Loading …" : "no results" + 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 …" @@ -84,13 +106,16 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { preconditionFailure() } + let sno = searchNo cell.imageView?.image = BundleIcon.image(bundleId) { - guard let url = altLoadUrl else { return } - BundleIcon.download(bundleId, urlStr: url) { + 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 @@ -103,14 +128,7 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { switch indexPath.section { case 0: guard indexPath.row < dataSource.count else { - 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) + showManualEntryAlert() return } let src = dataSource[indexPath.row] @@ -130,6 +148,8 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { 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 { @@ -140,18 +160,22 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { /// 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 { - dataSource = [] - tableView.reloadData() + setSource([], false) return } - AppStoreSearch.search(term) { - self.dataSource = $0 ?? [] + AppStoreSearch.search(term) { source, error in DispatchQueue.main.async { - self.tableView.reloadData() + setSource(source ?? [], error != nil) } } }