[UIKit] Spotify Clone: Search UI 3

Junyoung Park·2022년 9월 8일
0

UIKit

목록 보기
16/142
post-thumbnail

Building Spotify App in Swift 5 & UIKit - Part 16 - Search API (Xcode 12, 2021, Swift 5) - App

Spotify Clone: Search UI 3

구현 목표

  • 서치 뷰 컨트롤러의 서치 결과를 보여주는 뷰 컨트롤러 구현
  • 테이블 뷰를 통한 서치 결과 → 셀 클릭을 통한 디테일 데이터를 표현한 컨트롤러로 이동

구현 태스크

  1. 스포티파이 검색 API 함수 구현하기
  2. 검색 결과를 검색 결과 뷰 컨트롤러에게 전달
  3. 테이블 뷰 + 테이블 뷰 셀을 통한 검색 결과 UI 그리기
  4. 셀 클릭을 통한 현재 뷰에서 연관 뷰 컨트롤러 네비게이션 이동을 위한 델리게이트 패턴 구현

핵심 코드

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()
        }
    }
  • 검색 결과를 API로 리턴받고 검색 결과 뷰 컨트롤러에 해당 데이터를 로드하는 함수
  • 메인 스레드로 UI를 그리는 데 주의
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는 서치 뷰 컨트롤러 SearchViewControllersearchResultsController로 등록되어 있기 때문에 SearchViewController의 네비게이션 컨트롤러에 접근할 수 없음
  • 델리게이트 패턴을 통해 SearchViewControllerSearchResultsController 내 테이블 뷰 셀 클릭을 통해 알게 된 데이터를 넘겨받고 네비게이션 푸쉬

구현 화면

현재 뷰 컨트롤러가 특정 뷰 컨트롤러의 하단에 존재할 때 상단 뷰 컨트롤러의 네비게이션 컨트롤러를 사용하기 위해 델리게이트 패턴을 사용하고 있는데, presentingController를 통해 접근할 수도 있을 것 같다. 물론 부모/자식 뷰의 특정 컨트롤러를 사용하기 위해 접근한다는 점은 동일한 맥락이다.

profile
JUST DO IT

0개의 댓글