넷플릭스 화면 따라만들기 (2)
이전에는 searchBar를 구현했었는데, 이제는 searchTerm을 가지고 검색 API로 검색결과를 받아보는 과정을 구현해 볼 것이다.
먼저 어떤 task를 수행해야하는지 나열해보자.
- [목표] searchTerm을 가지고 네트워킹을 수행하여 영화를 검색해야한다.
- 그러기 위해서는 검색 API가 필요하다.
- 또한 검색 결과를 받아올 모델(Movie), Response가 필요하다.
- 마지막으로 결과를 받아와서, collectionView에 띄워야 한다.
말로는 매우 간단하다... 서버에서 키워드로 검색을하고, 결과를 받아와서, 원하는 정보만 앱 내에 띄워주면 된다는 것이다.
백문이불여일견...! 코드로 바로 가보자
import UIKit
import Kingfisher
class SearchViewController: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var resultCollectionView: UICollectionView!
var movies: [Movie] = []
override func viewDidLoad() {
super.viewDidLoad()
}
}
// 데이터
extension SearchViewController: UICollectionViewDataSource {
// 몇 개 넘어오니? (필수)
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return movies.count
}
// 어떻게 표현할꺼니? (필수) + 커스텀 셀 생성 필요
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) ->
UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ResultCell",
for: indexPath) as?
ResultCell else {
return UICollectionViewCell()
}
let movie = movies[indexPath.item]
let url = URL(string: movie.thumbnailPath)!
// imagePath -> image ( url로 네트워크 요청해서 받은 이미지 데이터를 UIImage로 만들어서 세팅해야함)
// 외부 코드 가져다 쓰기 (KingFisher)
// SPM(Swift Package Mananger), Cocoa Pod, Carthage
// https://github.com/onevcat/Kingfisher
cell.movieThumbnail.kf.setImage(with: url)
cell.backgroundColor = .red
return cell
}
}
// 클릭 되었을 때 구현해줘야 함
extension SearchViewController: UICollectionViewDelegate {
// 3개 표시, 양쪽 마진 8씩, 아이템 간에는 상하좌우 10
// w:h = 7:10
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let margin: CGFloat = 8
let itemSpacing: CGFloat = 10
// 양쪽 마진 빼주고, 3개 표시 위해 아이템간 거리 2개 제거 후 3등분
let width = (collectionView.bounds.width - margin * 2 - itemSpacing * 2) / 3
let height = width * 10/7
return CGSize(width: width, height: height)
}
}
// 각 셀의 크기 조절
extension SearchViewController: UICollectionViewDelegateFlowLayout {
}
class ResultCell: UICollectionViewCell {
@IBOutlet weak var movieThumbnail: UIImageView!
}
extension SearchViewController: UISearchBarDelegate {
private func dismissKeyboard() {
searchBar.resignFirstResponder()
// "너가 첫 번째 리스폰더가 아니니 사임해라" 라는 의마라고 함
// 첫 응답이 키보드가 올라오는 것인데,이를 관두면 키보드는 자동으로 내려가게 됨.
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
// search 버튼이 클릭되면 그 때 검색이 시작됨
// 키보드가 올라와 있을 때, 내려가게 처리 ( 검색을 눌렀을 때 결과가 잘 보이게 내려가도록! )
dismissKeyboard()
// 검색어가 있는지?
// 비어있지 않아야 한다.
guard let searchTerm = searchBar.text,
searchTerm.isEmpty == false else { return }
// 네트워킹을 통한 검색
// - [목표] searchTerm을 가지고 네트워킹을 통해 영화를 검색!
// - 검색API가 필요 (o)
// - 검색 결과를 받아올 모델(Movie 객체), Response이 필요 (o)
// - 결과를 받아와서, collectionView로 표현해주자.
SearchAPI.search(searchTerm) { movies in
// collectionView로 표현하기
// completion 핸들러로 넘겨주기에 completion(movies)가 여기서 수행된다.
print("--> counts : \(movies.count), 첫 제목: \(movies.first?.title)")
// 메인 스레드 조정 (네트워크 스레드에 있었어서)
DispatchQueue.main.async {
self.movies = movies
// collectionView의 데이터를 리프레쉬 하는 것
self.resultCollectionView.reloadData()
}
}
print("--> 검섹어 : \(searchTerm)")
// 텍스트가 없을 수 있어 optional로 내려준다!
// searchBar에 입력된 텍스트를 .text로 뽑아낼 수 있다.
}
}
class SearchAPI {
// type method : 인스턴스를 만들지 않더라고 클래스 타입에서 메서드를 바로 가져다 쓸 수 있음 SearchAPI.search(term: , completion: )
// @escaping : completion안에 있는 코드 블럭이 메서드 바깥에서 실행될 수도 있다는 것을 나타냄
static func search(_ term: String, completion: @escaping ([Movie]) -> Void) {
let session = URLSession(configuration: .default)
var urlComponents = URLComponents(string: "https://itunes.apple.com/search?")!
let mediaQuery = URLQueryItem(name: "media", value: "movie")
let entityQuery = URLQueryItem(name: "entity", value: "movie")
let termQuery = URLQueryItem(name: "term", value: term)
urlComponents.queryItems?.append(mediaQuery)
urlComponents.queryItems?.append(entityQuery)
urlComponents.queryItems?.append(termQuery)
let requestURL = urlComponents.url!
let dataTask = session.dataTask(with: requestURL) { (data, response, error) in
// 제대로 받아오면 아래 범위에 해당
let successRange = 200..<300
// dataTask이후 에러가 없어야 하고, 응답이 오더라도 서버에서 문제가 있는지 알려주는 statusCode를 받아와서 확인한다. ex) 404 : 페이지 못찾은 에러
guard error == nil, let statusCode = (response as? HTTPURLResponse)?.statusCode,
successRange.contains(statusCode) else {
return
completion([]) // 제대로 내려온 Movie 객체가 없다.
}
guard let resultData = data else {
completion([])
return
}
let movies = SearchAPI.parseMovies(resultData)
completion(movies)
// 오브젝트 파싱이 필요함! (Movie 객체)
// completion([Movie])
}
dataTask.resume() // 네트워킹 작업 시작!
}
// 파싱 메서드
// Data를 넣으면 Movie에 대한 정보로 파싱해준다.
static func parseMovies(_ data: Data) -> [Movie] {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(Response.self, from: data)
let movies = response.movies
return movies
} catch let error {
print("---> parsing error: \(error.localizedDescription)")
return [] // parsing이 안되어서 빈 어레이를 리텅
}
}
}
// json데이터를 쉽게 object로 파싱(decode)하기 위해 Codable을 사용한다.
struct Response: Codable {
let resultCount: Int
let movies: [Movie] // results, Movie객체 또한 Codable하게 만들어줘야 한다.
enum CodingKeys: String, CodingKey {
case resultCount
case movies = "results" // mapping
}
}
struct Movie: Codable {
let title: String
let director: String
let thumbnailPath: String
let previewURL: String
enum CodingKeys: String, CodingKey {
case title = "trackName"
case director = "artistName"
case thumbnailPath = "artworkUrl100"
case previewURL = "previewUrl" // key들을 매핑해주는 것
}
}
키워드를 통해 검색하고 서버에서 썸네일을 잘 불러온 것을 볼 수 있다.
끄읕.
** 패스트캠퍼스 iOS 앱 개발 올인원 패키지