거의 다 완성이 됐다. Home에서 컨테이너뷰로 액션영화와 가족영화를 여러 개 띄워주는 것에서 계속 오류가 나서 나머지부터 완성시켰다.
저번 포스트에선 SearchAPI를 통해 검색을 할 수 있게 하였는데 이제는 검색을 하면 ResultView에서 검색 결과를 CollectionView로 띄워준다.
셀을 누르면 해당 영화의 예고편이 재생되고, 일시정지가 가능하도록 하였다.
또한 Firebase의 RealtimeDatabase를 이용하여 SearchView에서 SearchBar아래에 테이블뷰로 검색결과를 띄우도록 하였다.
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라는 스토리보드에 영상 재생을 위한 뷰를 만들어주었고, 투명한 뷰를 위에 올려 재생버튼과 닫기 버튼을 구현하였다.
영상을 재생시키는 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이라는 프로퍼티는 따로 선언해주었다.(인터넷을 참고하였다.)
Firebase의 RealtimeDatabase를 이용해 검색어 저장 기능을 구현하였다.
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될 때 마다 순서가 바뀌기 때문이다.
현재 원하는대로 구현이 되지않는다. 생각대로라면 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공부도 같이 해야 할 것 같다.