// // SearchController.swift // TagTunes // // Created by Kim Wittenburg on 06.03.16. // Copyright © 2016 Kim Wittenburg. All rights reserved. // import SearchAPI import AppKitPlus /// A search delegate is notified when a `SearchController` selects an item. protocol SearchDelegate: class { /// The view controller displaying the content. var contentViewController: ContentViewController! { get } /// Invoked when the `searchController` selected a item. In the /// implementation the selected `searchResult` should be added to the /// `contentViewController`. func searchController(searchController: SearchController, didSelectSearchResult searchResult: TagTunesItem) } /// Manages the user interface for search results. public class SearchController: NSObject, PopUpSearchFieldDelegate { /// This struct contains *magic numbers* like references to storyboard /// identifiers. private struct Constants { /// The identifier used for the rows in the search results pop up. static let AlbumTableCellViewIdentifier = "AlbumTableCellViewIdentifier" /// The identifier used for the `No Results` row in the search results /// pop up. static let NoResultsTableCellViewIdentifier = "NoResultsTableCellViewIdentifier" } weak var delegate: SearchDelegate? /// The search field. This should be set by the view controller containing /// the user interface for searching. public weak var searchField: PopUpSearchField! { willSet { searchField?.popUpDelegate = nil } didSet { searchField?.popUpDelegate = self } } /// Used for searching and loading search results private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue()) /// The URL task currently loading the search results. private var searchTask: NSURLSessionTask? { didSet { searching = searchTask != nil } } public private(set) dynamic var searching: Bool = false /// The search results for the current search term. private var searchResults = [Album]() { didSet { searchField.updatePopUp() } } /// The error that occured during searching, if any. private var searchError: NSError? /// Begins searching for the `searchField`'s `stringValue`. public func beginSearch() { let searchString = searchField.stringValue cancelSearch() if searchString == "" { searchResults = [] } else { var request = SearchAPIRequest(searchRequestWithTerm: searchString) request.mediaType = .Music request.entity = .Album request.maximumNumberOfResults = UInt(Preferences.sharedPreferences.numberOfSearchResults) if Preferences.sharedPreferences.useEnglishTags { request.language = .English } request.country = Preferences.sharedPreferences.iTunesStore searchTask = urlSession.dataTaskWithURL(request.URL, completionHandler: processSearchResults) searchTask!.resume() } } /// Cancels the current search (if there is one) and removes the search /// results. private func cancelSearch() { searchTask?.cancel() } /// Processes the data returned from a network request into the /// `searchResults` array. private func processSearchResults(data: NSData?, response: NSURLResponse?, error: NSError?) { searchTask = nil var newResults = [Album]() if let theData = data where error == nil { do { let result = try SearchAPIResult(data: theData) newResults = result.resultEntities.flatMap{ $0 as? Album } } catch let error as NSError { searchError = error } } else if let theError = error { searchError = theError } searchResults = newResults } public func popUpSearchFieldWillHidePopUp(searchField: PopUpSearchField) { cancelSearch() } public func popUpSearchFieldShouldShowPopUp(searchField: PopUpSearchField) -> Bool { return !searchField.stringValue.isEmpty } public func popUpSearchField(searchField: PopUpSearchField, didSelectPopUpEntryAtRow row: Int) { let searchResult = searchResults[row] let albumItem = AlbumItem(entity: searchResult) albumItem.beginLoadingChildren() delegate?.searchController(self, didSelectSearchResult: albumItem) searchField.reloadPopUpRow(row) } private var shouldDisplayNoResults: Bool { return searchResults.isEmpty && !searchField.stringValue.isEmpty && searchTask == nil } public func numberOfRowsInTableView(tableView: NSTableView) -> Int { if shouldDisplayNoResults { return 1 } return searchResults.count } public func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 39 } public func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { if shouldDisplayNoResults { var view = tableView.makeViewWithIdentifier(Constants.NoResultsTableCellViewIdentifier, owner: nil) as? CenteredTableCellView if view == nil { view = CenteredTableCellView() view?.identifier = Constants.NoResultsTableCellViewIdentifier } view?.setupForNoResults() return view } else { var view = tableView.makeViewWithIdentifier(Constants.AlbumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView if view == nil { view = AlbumTableCellView() view?.identifier = Constants.AlbumTableCellViewIdentifier } let searchResult = searchResults[row] let height = self.tableView(tableView, heightOfRow: row) let albumEnabled = delegate?.contentViewController.itemForEntity(searchResult) == nil view?.setupForAlbum(searchResult, enabled: albumEnabled, height: height) return view } } public func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool { if shouldDisplayNoResults { return false } else { return delegate?.contentViewController?.itemForEntity(searchResults[row]) == nil } } }