TMDB TV Series Search
URL
import Foundation
extension URL {
static let imageURL = "https://image.tmdb.org/t/p/w600_and_h900_bestv2"
static func makeImageUrl(_ endpoint: String) -> URL {
guard let url = URL(string: imageURL + endpoint) else { return URL(string: "")! }
return url
}
static let baseUrl = "https://api.themoviedb.org/3/tv/"
static func makeSeasonUrl(_ seriesId: Int) -> String {
return baseUrl + "\(seriesId)?language=ko-KO"
}
static func makeEpisodeUrl(_ seriesId: Int, _ season: Int) -> String {
return baseUrl + "\(seriesId)/season/\(season)?language=ko-KO"
}
static func makeSearchingUrl(_ query: String) -> String {
guard let txt = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return ""}
return "https://api.themoviedb.org/3/search/tv?query=" + txt
}
}
APIManager
import Foundation
import Alamofire
import SwiftyJSON
class TmdbAPIManager {
static let shared = TmdbAPIManager()
private init() {}
let header: HTTPHeaders = ["Authorization" : Key.tmdb]
func callSeason(_ seriesId: Int, completionHandler: @escaping (Tv) -> Void ) {
let url = URL.makeSeasonUrl(seriesId)
AF.request(url, method: .get, headers: header)
.validate(statusCode: 200...500)
.responseDecodable(of: Tv.self) { response in
let statusCode = response.response?.statusCode ?? 500
if (statusCode == 200) {
guard let value = response.value else { return }
completionHandler(value)
} else {
print("Error!! statusCode : \(statusCode)")
print(response)
}
}
}
func callEpisode(_ seriesId: Int, _ season: Int, completionHandler: @escaping (Tvdetail)-> Void) {
let url = URL.makeEpisodeUrl(seriesId, season)
AF.request(url, method: .get, headers: header)
.validate(statusCode: 200...500)
.responseDecodable(of: Tvdetail.self) { response in
let statusCode = response.response?.statusCode ?? 500
if (statusCode == 200) {
guard let value = response.value else { return }
completionHandler(value)
} else {
print("Error!! statusCode : \(statusCode)")
print(response)
}
}
}
func callSearch(_ query: String, completionHandler: @escaping (Searching) -> Void) {
let url = URL.makeSearchingUrl(query)
AF.request(url, method: .get, headers: header)
.validate(statusCode: 200...500)
.responseDecodable(of: Searching.self) { response in
let statusCode = response.response?.statusCode ?? 500
if (statusCode == 200) {
guard let value = response.value else { return }
completionHandler(value)
} else {
print("Error!! statusCode : \(statusCode)")
print(response)
}
}
}
}
MainViewController
import UIKit
class MainViewController: UIViewController {
@IBOutlet var mainSearchBar: UISearchBar!
@IBOutlet var mainCollectionView: UICollectionView!
var seasonInfo: Tv?
var episodeInfo: [Tvdetail] = []
var seriesId = 113268
var isSeason0 = false
override func viewDidLoad() {
super.viewDidLoad()
checkSeason0()
mainCollectionView.keyboardDismissMode = .onDrag
configureSearchBar()
configureCollectionView()
configureCollectionViewLayout()
}
func checkSeason0() {
if (seasonInfo?.numberOfSeasons != seasonInfo?.seasons.count) {
isSeason0 = true
} else {
isSeason0 = false
}
}
func callSeasonRequest(_ seriesId: Int) {
TmdbAPIManager.shared.callSeason(seriesId) { value in
self.seasonInfo = value
print(self.seasonInfo?.numberOfSeasons)
print(self.seasonInfo?.seasons.count)
self.checkSeason0()
guard let seasonCnt = self.seasonInfo?.seasons.count else { return }
for _ in 1...(seasonCnt + 1) {
self.episodeInfo.append(Tvdetail(id: "", airDate: "", episodes: [], name: "", overview: "", tvdetailID: 0, posterPath: "", seasonNumber: 0, voteAverage: 0))
}
let group = DispatchGroup()
guard let allSeasons = self.seasonInfo?.seasons else { return }
for item in allSeasons {
group.enter()
self.callEpisodeRequest(seriesId, item.seasonNumber) {
group.leave()
}
}
group.notify(queue: .main) {
self.mainCollectionView.reloadData()
}
}
}
func callEpisodeRequest(_ seriesId: Int, _ season: Int, completionHandler: @escaping ()-> Void ) {
TmdbAPIManager.shared.callEpisode(seriesId, season) { value in
self.episodeInfo[season] = value
completionHandler()
}
}
func callSearchList(_ query: String) {
TmdbAPIManager.shared.callSearch(query) { value in
if !value.results.isEmpty {
self.seriesId = value.results[0].id
self.callSeasonRequest(self.seriesId)
}
}
}
}
extension MainViewController: CollectionViewAttributeProtocol {
func configureSearchBar() {
mainSearchBar.delegate = self
}
func configureCollectionView() {
mainCollectionView.dataSource = self
mainCollectionView.delegate = self
mainCollectionView.register(
UINib(nibName: SeasonCollectionReusableView.identifier, bundle: nil),
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: SeasonCollectionReusableView.identifier
)
mainCollectionView.register(
UINib(nibName: EpisodeCollectionViewCell.identifier, bundle: nil ),
forCellWithReuseIdentifier: EpisodeCollectionViewCell.identifier
)
}
func configureCollectionViewLayout() {
let spacing: CGFloat = 10
let width = UIScreen.main.bounds.width
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: width, height: 100)
layout.minimumLineSpacing = spacing
layout.minimumInteritemSpacing = spacing
layout.scrollDirection = .vertical
layout.headerReferenceSize = CGSize(width: width, height: 50)
mainCollectionView.collectionViewLayout = layout
}
}
extension MainViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
guard let seasonInfo else { return 0 }
return seasonInfo.seasons.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if (isSeason0) {
return episodeInfo[section].episodes.count
} else {
return episodeInfo[section+1].episodes.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EpisodeCollectionViewCell.identifier, for: indexPath) as? EpisodeCollectionViewCell else { return UICollectionViewCell() }
if (isSeason0) {
cell.designCell(episodeInfo[indexPath.section].episodes[indexPath.row])
}
else {
cell.designCell(episodeInfo[indexPath.section+1].episodes[indexPath.row])
}
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if (kind == UICollectionView.elementKindSectionHeader) {
guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SeasonCollectionReusableView.identifier, for: indexPath) as?
SeasonCollectionReusableView else { return UICollectionReusableView() }
view.designCell((seasonInfo?.seasons[indexPath.section])!)
return view
}
return UICollectionReusableView()
}
}
extension MainViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let query = searchBar.text else { return }
if query != "" {
callSearchList(query)
}
}
}