183 lines
6.5 KiB
Swift
Executable File
183 lines
6.5 KiB
Swift
Executable File
//
|
|
// 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
|
|
}
|
|
}
|
|
|
|
}
|