[Swift] 넷플릭스 화면 따라만들기 (2)
👨🏻‍💻iOS 공부/iOS & Swift

[Swift] 넷플릭스 화면 따라만들기 (2)

728x90
반응형

넷플릭스 화면 따라만들기 (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 앱 개발 올인원 패키지











728x90
반응형