Building Spotify App in Swift 5 & UIKit - Part 16 - Search API (Xcode 12, 2021, Swift 5) - App
public func search(with query: String, completionHandler: @escaping (Result<[SearchResult], Error>) -> ()) {
createRequest(with: URL(string: Constants.baseAPIURL + "/search?limit=10&type=album,artist,playlist,track&q=\(query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"), type: .GET) { request in
URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
error == nil else {
completionHandler(.failure(APIError.failedToGetData))
return
}
do {
let result = try JSONDecoder().decode(SearchResultsResponse.self, from: data)
var searchResults: [SearchResult] = []
searchResults.append(contentsOf: result.tracks.items.compactMap({ SearchResult.track(model: $0)}))
searchResults.append(contentsOf: result.albums.items.compactMap({ SearchResult.album(model: $0)}))
searchResults.append(contentsOf: result.artists.items.compactMap({ SearchResult.artist(model: $0)}))
searchResults.append(contentsOf: result.playlists.items.compactMap({ SearchResult.playlist(model: $0)}))
completionHandler(.success(searchResults))
} catch {
print(error.localizedDescription)
completionHandler(.failure(error))
}
}
.resume()
}
}
enum SearchResult {
case artist(model: Artist)
case album(model: Album)
case track(model: AudioTrack)
case playlist(model: Playlist)
}
func update(with results: [SearchResult]) {
let artists = results.filter({
switch $0 {
case .artist: return true
default: return false
}
})
let albums = results.filter({
switch $0 {
case .album: return true
default: return false
}
})
let tracks = results.filter({
switch $0 {
case .track: return true
default: return false
}
})
let playlists = results.filter({
switch $0 {
case .playlist: return true
default: return false
}
})
self.sections = [
SearchSection(title: "Artists", results: artists),
SearchSection(title: "Albums", results: albums),
SearchSection(title: "Tracks", results: tracks),
SearchSection(title: "Playlists", results: playlists)
]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.tableView.isHidden = self.sections.isEmpty
self.tableView.reloadData()
}
}
protocol SearchResultsViewControllerDelegate: AnyObject {
func didTapResult(_ result: SearchResult)
}
class SearchResultsViewController: UIViewController {
weak var delegate: SearchResultsViewControllerDelegate?
...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let result = sections[indexPath.section].results[indexPath.row]
delegate?.didTapResult(result)
}
}
extension SearchViewController: SearchResultsViewControllerDelegate {
func didTapResult(_ result: SearchResult) {
switch result {
case .artist(model: let model):
break
case .album(model: let model):
let vc = AlbumViewController(album: model)
vc.navigationItem.largeTitleDisplayMode = .never
navigationController?.pushViewController(vc, animated: true)
case .track(model: let model):
break
case .playlist(model: let model):
let vc = PlaylistViewController(playlist: model)
vc.navigationItem.largeTitleDisplayMode = .never
navigationController?.pushViewController(vc, animated: true)
}
}
}
SearchResultsViewController
는 서치 뷰 컨트롤러 SearchViewController
의 searchResultsController
로 등록되어 있기 때문에 SearchViewController
의 네비게이션 컨트롤러에 접근할 수 없음SearchViewController
가 SearchResultsController
내 테이블 뷰 셀 클릭을 통해 알게 된 데이터를 넘겨받고 네비게이션 푸쉬현재 뷰 컨트롤러가 특정 뷰 컨트롤러의 하단에 존재할 때 상단 뷰 컨트롤러의 네비게이션 컨트롤러를 사용하기 위해 델리게이트 패턴을 사용하고 있는데,
presentingController
를 통해 접근할 수도 있을 것 같다. 물론 부모/자식 뷰의 특정 컨트롤러를 사용하기 위해 접근한다는 점은 동일한 맥락이다.