[Toy] 영화 예고편 어플 - 2

희희희·2022년 1월 4일
0

투자한 시간이 적기도 하고 자꾸 다른 길로 새다보니 이번 포스트 적는데까지 너무 오래 걸린 것 같다. 다음 포스트까지는 프로젝트를 끝낼 수 있을 것 같다. 굉장히 간단한 프로젝트라고 생각하는데 길게 끌게 된 것에 대해 반성해야겠다.


진행된 상태

저번 포스트에선 SearchViewController와 ResultViewController의 스토리보드만 구성해주었다.

이제는 SearchView에서 검색어를 입력하면 ResultView를 띄우고 검색어를 라벨에 띄우도록하였다. 아직까지 검색 결과를 CollectionView에 띄우는데까진 못하였고, print함수를 통해 검색 결과의 수를 보여주도록 하였다.

검색을 위해서는 itunes에서 제공해주는 iTunes search API를 이용하도록 하였다.

아래처럼 검색어를 입력하면 ResultView를 띄우면서 검색어를 라벨로 받아오는 것을 볼 수 있으며, 검색 결과가 몇 개인지 print되는 것을 볼 수 있다.

진행한 것들을 하나씩 아래에서 정리해보겠다.


- iTunes Search API

먼저 iTunes Search API를 간단히 설명하면 애플에서 제공해주는 API로 website를 통해 iTunes Store, App Store, iBooks, 그리고 Mac App Store의 컨텐츠를 검색할 수 있게 해주는 API이다.

이번 프로젝트에서 영화를 검색할 수 있도록 API를 만들 것인데 위의 iTunes Search API를 이용하여 영화에 대한 정보를 얻어올 것이다.


ResultViewController

  • Storyboard 구성

아래처럼 검색 결과 화면에서 한 줄에 두 개의 결과를 띄우도록 했다.

cell사이의 spacing과 양 옆의 margin을 전체 view의 width에서 빼주고 2로 나눠주면 되겠다고 생각했다.

  • Code

UICollectionViewDelegateFlowLayout 클래스를 이용하여 collectionView의 width에서 2개의 margin과 1개의 spacing을 뺀 뒤 2로 나누어 cell의 크기를 계산해주었다.

extension ResultViewController: UICollectionViewDelegateFlowLayout

{
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let margin: CGFloat = 10
        let spacing: CGFloat = 15
        let width: CGFloat = (collectionView.bounds.width - 2 * margin - spacing) / 2
        let height: CGFloat = width * 10 / 7
        
        return CGSize(width: width, height: height)
        
    }
}

SearchViewController

위의 영상에서 볼 수 있듯이 검색어를 입력하고 엔터를 누르면 ResultView를 띄운다.

또한 SearchAPI가 어떤 식으로 이루어져있는지 확인해보자.

  • Code

먼저 ResultView를 띄우는 코드이다.

extension SearchViewController: UISearchBarDelegate {
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        
        
        //search 했을때 ResultView를 띄우기위해
        let sb = UIStoryboard(name: "Result", bundle: nil)
        let vc = sb.instantiateViewController(identifier: "ResultView") as! ResultViewController
        vc.modalPresentationStyle = .fullScreen
        
        guard let searchTerm = searchBar.text, searchTerm.isEmpty == false else {return}
        
        self.present(vc, animated: true, completion: nil)
        
        vc.searchLabel.font = UIFont.systemFont(ofSize: 20, weight: .light)
        vc.searchLabel.text = searchTerm
        
        
        SearchAPI.search(searchTerm) { movies in
            print("\(movies.count)")
        }
        
    }
}

searchBarSearchButtonClicked란 함수는 UISearchBar에서 Search를 했을 때 작동되는 코드이다. 즉, 검색어를 입력하여 검색을 하게되는 순간 함수 안의 코드들이 실행된다.

나는 보기 편하게 하기 위해 스토리보드 파일을 추가하여 ResultViewController을 따로 만들어주었다.

위의 코드는 스토리보드 파일에서 원하는 뷰컨트롤러의 뷰를 보여준다.

코드 마지막부분에서 SearchAPI를 이용하는 것을 볼 수 있다. 검색어를 입력하고 검색이 클릭되면 API를 통해 searchTerm에 대해서 검색한 뒤 movies의 count를 출력함으로써 결과 개수를 얻을 수 있다.


SearchAPI

  • Code
class SearchAPI {
    static func search(_ searchTerm: 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: searchTerm)
        
        urlComponents.queryItems?.append(mediaQuery)
        urlComponents.queryItems?.append(entityQuery)
        urlComponents.queryItems?.append(termQuery)
        
        guard let requestURL = urlComponents.url else { return  }

        
        let dataTask = session.dataTask(with: requestURL) { data, response, error in // 세가지가 내려옴
            let successRange = 200..<300
            
            guard error == nil, let statusCode = (response as? HTTPURLResponse)?.statusCode,
                  successRange.contains(statusCode) else {
                      completion([]) //제대로된 데이터가 없다.
                      return
                  } //성공적으로 response가 왔는지
            
            guard let resultData = data else {
                completion([])
                return
            }
            //print(resultData)
            
            //data를 파싱해서 [Movie]로 해줌
	      //let string = String(data: resultData, encoding: .utf8)
//            print(string)
            
            let movies = SearchAPI.parseMovies(resultData)
            completion(movies)
        }
        dataTask.resume()
    }
    //파싱하기
    static func parseMovies(_ data: Data) -> [Movie] {
        let decoder = JSONDecoder()
        
        do{
            let response = try decoder.decode(Response.self, from: data) //될지 안될지 모르기때문에 try
            let movies = response.movies
            print("good")
            return movies
            
        } catch let DecodingError.dataCorrupted(context) {
            print(context)
            return []
        } catch let DecodingError.keyNotFound(key, context) {
            print("Key '\(key)' not found:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []

        } catch let DecodingError.valueNotFound(value, context) {
            print("Value '\(value)' not found:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []

        } catch let DecodingError.typeMismatch(type, context)  {
            print("Type '\(type)' mismatch:", context.debugDescription)
            print("codingPath:", context.codingPath)
            return []

        } catch {
            print("error: ", error)
            return []

        }
    }
}

네트워킹을 통해 데이터를 받아오려고 할 때 URL을 이용한다.

iTunesSearchAPI에서 원하는 검색을 하기 위해 URL을 확인한 뒤 queryItems으로 원하는 결과를 얻을 수 있게 requestURL을 만들어주도록했다.

dataTask를 이용하면 data, response, error를 받아오는데 response에는 statusCode가 있다. 200에서 300사이가 성공범위이므로 response가 잘 도착했는지 확인하도록 하였다.

받은 data는 json형태이다. 따라서 JSONDecoder를 이용하여 데이터를 파싱해야한다. 이때 여러가지 실패 경우의 수가 있을 것이므로 어떤 문제인지 파악하기위해 catch부분에 다양한 error들을 알려주도록 작성하였다.

type메소드를 이용하여 작성하였기 때문에 생성자() 를 통해서 접근하지 않아도 바로 접근이 가능한 것을 볼 수 있다.

  • 모델
struct Response: Codable {
    let resultCount: Int
    let movies: [Movie]
    
    enum CodingKeys: String, CodingKey {
        case resultCount
        case movies = "results"
    }
}
struct Movie: Codable {
    let title: String
    let director: String
    let thumbnailPath: String
    let trailerURL: String
    let genre: String
    
    enum CodingKeys: String, CodingKey {
        case title = "trackName"
        case director = "artistName"
        case thumbnailPath = "artworkUrl100"
        case trailerURL = "previewUrl"
        case genre = "primaryGenreName"
    }
}

파싱하기 위해 Codable로 Response와 Movie가 선언된 것을 볼 수 있다.
위의 코드를 간단히 설명하자면 실제로 받아오는 데이터 안에서는 title, director, 등의 인스턴스들이 각각 대응되는 String으로 표현되어있다.

즉, CodingKey를 이용해 위와 같이 작성해준다면 실제 데이터에 작성된 이름이 아닌 우리가 원하는 이름으로 사용할 수 있는 것이다.


아직 여러가지 과제들이 남아있다. 받아온 데이터들을 ResultView에 표현해야하며 각 셀을 눌렀을 때 데이터로 받아온 previewURL을 통해 예고편 동영상을 실행시킬 수 있어야한다. 또한, 위와 같은 네트워킹으로 genre로 분류하여 Home에서 임의로 몇 개를 보여줘야한다.

프로젝트를 하며 다양한 기술들이 사용된 것에 비해 내가 정리해놓은 것이 없다. 배웠던 기술이나 swift문법에 대해서 따로 차근차근 정리하지않으면 분명 사용할 때마다 헷갈릴 일이 많아질 것 같다. 열심히 해야겠다.

profile
iOS 어플 개발 연습

0개의 댓글