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

희희희·2022년 1월 7일
0

진행된 상태

거의 다 완성이 됐다. Home에서 컨테이너뷰로 액션영화와 가족영화를 여러 개 띄워주는 것에서 계속 오류가 나서 나머지부터 완성시켰다.

저번 포스트에선 SearchAPI를 통해 검색을 할 수 있게 하였는데 이제는 검색을 하면 ResultView에서 검색 결과를 CollectionView로 띄워준다.

셀을 누르면 해당 영화의 예고편이 재생되고, 일시정지가 가능하도록 하였다.

또한 Firebase의 RealtimeDatabase를 이용하여 SearchView에서 SearchBar아래에 테이블뷰로 검색결과를 띄우도록 하였다.

  • 제대로 구현되지 않은 Home
    받은 데이터를 나타내지 못하는 상태
  • 구현 완료

ResultViewController

SearchBar에 검색어를 입력하면 ResultView에서 검색결과를 컬렉션뷰로 띄워준다. 이때 셀을 눌렀을 때 예고편이 재생되도록 하였다.

  • 전체 코드
import UIKit
import Kingfisher
import AVFoundation

class ResultViewController: UIViewController {

    @IBOutlet weak var searchLabel: UILabel!
    @IBOutlet weak var resultCollectionView: UICollectionView!
    
    var movies: [Movie] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
    @IBAction func dismissBtn(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    

}

extension ResultViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let movie = movies[indexPath.row]
        let url = URL(string: movie.trailerURL)!
        let item = AVPlayerItem(url: url)
        
        let sb = UIStoryboard(name: "Player", bundle: nil)
        let vc = sb.instantiateViewController(withIdentifier: "Player") as! PlayerViewController
        
        vc.player.replaceCurrentItem(with: item)
        vc.modalPresentationStyle = .fullScreen
        self.present(vc, animated: true, completion: nil)
    
    }
}

extension ResultViewController: 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: "cell", for: indexPath) as? ResultCell else { return UICollectionViewCell() }
        
        let movie = movies[indexPath.row]
        let url = URL(string: movie.thumbnailPath)
        cell.thumbnailImage.kf.setImage(with: url)
        
        cell.layer.cornerRadius = 15
        cell.layer.borderWidth = 1
        cell.layer.borderColor = UIColor.black.cgColor
        
        return 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)
        
    }
}

class ResultCell: UICollectionViewCell {
    @IBOutlet weak var thumbnailImage: UIImageView!
}

간단히 설명하면 먼저 SearchViewCotroller의 searchBarSearchButtonClicked함수에서 Movie 구조의 movies에 데이터를 넘겨주었다.

이 데이터엔 주소 형식의 썸네일 이미지와 예고편 영상이 담겨있다.

각각 AVFoundation와 외부 라이브러리인 Kingfisher를 이용하여 이미지를 나타내고, 동영상을 재생시키도록 하였다.

Player라는 스토리보드에 영상 재생을 위한 뷰를 만들어주었고, 투명한 뷰를 위에 올려 재생버튼과 닫기 버튼을 구현하였다.

  • AVFoundation
    -> Apple에서 제공하는 미디어 프레임워크이다. 개발을 할 때 미디어와 관련된(동영상 재생, 음악 재생 등) 것들에는 빠지지않고 사용된다.
  • Kingfisher
    -> 오픈소스 라이브러리로 이미지를 간편하게 관리할 수 있게 해준다.

PlayerViewController

영상을 재생시키는 Player의 뷰컨트롤러이다. 처음 영상이 재생될 때 landscape를 이용해 가로 모드로 띄울 수 있게 하였고, 재생버튼과 닫기 버튼을 구현하였다.

  • 전체 코드
import UIKit
import AVFoundation

class PlayerViewController: UIViewController {

    
    @IBOutlet weak var playerView: PlayerView!
    @IBOutlet weak var controlView: UIView!
    @IBOutlet weak var playButton: UIButton!
    
    let player = AVPlayer()
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .landscapeRight
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        playerView.player = player

    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        play()
    }
    
    @IBAction func togglePlayButton(_ sender: Any) {
        if player.isPlaying {
            pause()
        } else {
            play()
        }
    }
    
    func play() {
        player.play()
        playButton.isSelected = true

    }
    func pause() {
        player.pause()
        playButton.isSelected = false

    }
    func reset() {
        pause()
        player.replaceCurrentItem(with: nil)
    }

    @IBAction func closeButton(_ sender: Any) {
        reset()
        dismiss(animated: true, completion: nil)
    }
}

extension AVPlayer {
    var isPlaying: Bool {
        guard self.currentItem != nil else {
            return false}
        return self.rate != 0
    }
}

play와 pause함수를 선언하여 영상 재생과 버튼의 모양(재생 / 일시정지)를 표현할 수 있도록 하였다.

재생 중인 것을 나타내는 isPlaying이라는 프로퍼티는 따로 선언해주었다.(인터넷을 참고하였다.)


SearchViewController

Firebase의 RealtimeDatabase를 이용해 검색어 저장 기능을 구현하였다.

  • 코드(SearchAPI는 저번 포스트에 있어 제외하였다.)
import UIKit
import Kingfisher
import Firebase

class SearchViewController: UIViewController {
    @IBOutlet weak var searchHistory: UITableView!
    
    let db = Database.database().reference().child("searchHistory")
    var searchTerms: [SearchTerm] = []
    
    
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()

        
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        db.observeSingleEvent(of: .value) { snapshot in
            guard let searchHistory = snapshot.value as? [String: Any] else { return } //key를 제외한 value값만 보기위해
            let datas = Array(searchHistory.values)
            let decoder = JSONDecoder()
            
            
            let data = try! JSONSerialization.data(withJSONObject: datas, options: [])//JSON형태로 만들어줌
            let searchTerms = try! decoder.decode([SearchTerm].self, from: data)
            
            self.searchTerms = searchTerms.sorted(by: { (term1, term2) in
                return term1.timestamp > term2.timestamp
            })
            self.searchHistory.reloadData()
            print("\(data), \(searchTerms)")

        }
    }
}

extension SearchViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchTerms.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? HistoryCell else { return UITableViewCell() }
        
        cell.resultLabel.text = searchTerms[indexPath.row].term
        
        return cell
    }

}

extension SearchViewController: UITableViewDelegate {
    
}

//SearchBar
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
            DispatchQueue.main.async {
                vc.movies = movies
                print(vc.movies.count)
                vc.resultCollectionView.reloadData()
                
                let timestamp:Double = Date().timeIntervalSince1970.rounded()
                self.db.childByAutoId().setValue(["term": searchTerm, "timestamp": timestamp])
            }
        }
        
        
    }
}

class HistoryCell: UITableViewCell {
    @IBOutlet weak var resultLabel: UILabel!
}

struct SearchTerm: Codable {
    let term: String
    let timestamp: Double
}

Firebase를 import하고 database를 만들어주었다.

[key:
{term ,
timestamp}]

이러한 Dictionary형식으로 저장이 된다.

Json형태의 데이터를 파싱하여 searchTerms라는 배열에 저장한 뒤, timestamp값을 이용해 sorting하고 term을 테이블뷰 셀의 라벨로 띄워주었다.

sorting하지않으면 reload될 때 마다 순서가 바뀌기 때문이다.


HomeViewController

현재 원하는대로 구현이 되지않는다. 생각대로라면 SearchAPI에서 데이터를 받아오는 것과 동일하게 Action, Family장르의 데이터를 받아와 이를 이용해 썸네일 이미지와 동영상 재생이 가능하게 해야하는데 계속 오류가 나고있다. 비동기처리와 관련한 오류도 뜨다가, 뜨지 않을 땐 데이터가 제대로 전달되지 않아 더 생각해봐야 할 것 같다.

  • 전체 코드
import UIKit

class HomeViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if titleLabel.adjustsFontSizeToFitWidth == false {
            titleLabel.adjustsFontSizeToFitWidth = true
        }
        
        Avengers.sample { movies in
            let sb = UIStoryboard(name: "Main", bundle: nil)
            
            DispatchQueue.main.async {
                let vc = sb.instantiateViewController(withIdentifier: "Avengers") as! AvengersViewController
                self.present(vc, animated: false, completion: nil)
                
                vc.avengersMovies = movies
                print(vc.avengersMovies.count)
                
            }
        }
        
        DispatchQueue.main.async {
            FamilyMovie.family { movies in
                let sb = UIStoryboard(name: "Main", bundle: nil)
                let vc = sb.instantiateViewController(withIdentifier: "Family") as? FamilyViewController

                vc?.familyMovies = movies
            }
        }
        
    }
    
}

//Action이었지만 previewURL이 없는 것들이 있어 Avengers로 바꿈
class Avengers {
    
    static func sample(completion: @escaping ([Movie]) -> Void) {
        
        let session = URLSession(configuration: .default)
        var urlComponent = 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: "Avengers")

        urlComponent?.queryItems?.append(mediaQuery)
        urlComponent?.queryItems?.append(entityQuery)
        urlComponent?.queryItems?.append(termQuery)
        
        guard let requestURL = urlComponent?.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
            }
            
            guard let resultData = data else {
                completion([])
                return
            }
            
            let movies = Avengers.parsingData(resultData)
            completion(movies)
        }
        dataTask.resume()
        
    }
    static func parsingData(_ data: Data) -> [Movie] {
        let decoder = JSONDecoder()
        
        do {
            let response = try decoder.decode(Response.self, from: data)
            let movies = response.movies
            
            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 []

        }
    }
}

//Family였지만 previewURL이 없는 것들이 있어 Dark Knight로 바꿈
class DarkKnight {
    static func sample(completion: @escaping ([Movie]) -> Void) {
        
        let session = URLSession(configuration: .default)
        var urlComponent = 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: "dark-knight")

        urlComponent?.queryItems?.append(mediaQuery)
        urlComponent?.queryItems?.append(entityQuery)
        urlComponent?.queryItems?.append(termQuery)
        
        guard let requestURL = urlComponent?.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
            }
            
            guard let resultData = data else {
                completion([])
                return
            }
            
            let movies = DarkKnight.parsingData(resultData)
            completion(movies)
        }
        dataTask.resume()
        
    }
    static func parsingData(_ data: Data) -> [Movie] {
        let decoder = JSONDecoder()
        
        do {
            let response = try decoder.decode(Response.self, from: data)
            let movies = response.movies
            
            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 []

        }
    }
}

일단 위의 코드는 오류가 난 상태로 이것저것 해보다가 올리는 코드이다. 검색할 때 previewURL이 없는 데이터들이 있어 각각 Avengers와 Darkknight로 바꾼 상태이다. 1월 7일까지 끝내려고 했으나 해결 안 되는 부분은 좀 더 생각해봐야 할 것 같다.


원하던 기능들은 다 구현이 된 상태이다. 해결 안된 부분도 기능만 보면 이미 구현이 됐던 것으로 구현하는 것이다. 대부분 완료 됐으니 이 프로젝트의 고민과 함께 코딩테스트와 CS공부도 같이 해야 할 것 같다.


github

profile
iOS 어플 개발 연습

0개의 댓글