구현 내용
- Home 화면 내에 보이는 영화 리스트를 누르면 해당 영화에 대한 상세페이지로 넘어간다.
- 상세 페이지 안에는 영화와 관련된 예고편과 영화 제목, 영화 개요 및 다운로드 버튼이 있다.
구현 코드
- 상세 페이지 관련한 코드를 담은 TitlePreviewViewController.swift 파일을 생성한다.
import UIKit
import WebKit
class TitlePreviewViewController: UIViewController {
// MARK: UI Components
private let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 22, weight: .bold)
label.text = "Harry Potter"
return label
}()
private let overViewLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = "이거 처음 본게 엊그제 같은데 벌써 20년이 되가네 대단한 작품이지 재밌었어"
return label
}()
private let downloadButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .red
button.setTitle("Download", for: .normal)
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 12
button.layer.masksToBounds = true
return button
}()
private let webView: WKWebView = {
let webView = WKWebView()
webView.translatesAutoresizingMaskIntoConstraints = false
return webView
}()
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(webView)
view.addSubview(titleLabel)
view.addSubview(overViewLabel)
view.addSubview(downloadButton)
configureConstraints()
}
// MARK: Functionts
func configure(with model: TitlePreviewViewModel) {
titleLabel.text = model.title
overViewLabel.text = model.titleOverview
guard let url = URL(string: "https://www.youtube.com/embed/\(model.youtubeView.id.videoId)") else { return }
webView.load(URLRequest(url: url))
}
// MARK: UI Constraints
private func configureConstraints() {
let webViewConstraints = [
webView.topAnchor.constraint(equalTo: view.topAnchor,constant: 50),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.heightAnchor.constraint(equalToConstant: 300)
]
let titleLabelConstraints = [
titleLabel.topAnchor.constraint(equalTo: webView.bottomAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
]
let overViewLabelConstraints = [
overViewLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15),
overViewLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 15),
overViewLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15)
]
let downloadButtonConstraints = [
downloadButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
downloadButton.topAnchor.constraint(equalTo: overViewLabel.bottomAnchor, constant: 20),
downloadButton.widthAnchor.constraint(equalToConstant: 120),
downloadButton.heightAnchor.constraint(equalToConstant: 45)
]
NSLayoutConstraint.activate(webViewConstraints)
NSLayoutConstraint.activate(titleLabelConstraints)
NSLayoutConstraint.activate(overViewLabelConstraints)
NSLayoutConstraint.activate(downloadButtonConstraints)
}
}
- APICaller - getMovie 라는 메서드를 통해 얻어올 데이터 중에서 필요한 부분만 별도의 viewModel로 생성한다.
import Foundation
struct TitlePreviewViewModel {
let title: String
let youtubeView: VideoElement
let titleOverview: String
}
이 ViewModel을 사용하는 주요 이유는 다음과 같다.
- 다중 데이터 소스 통합:
- Title 모델에서 영화/TV 쇼의 제목과 개요를 가져온다.
- YouTube API 응답에서 관련 비디오 정보를 가져오는데, 이 두 가지 다른 데이터 소스의 정보를 하나의 ViewModel로 통합하여 사용하기 위함이다.
- 데이터 변환 및 가공:
- YouTube API 응답에서 필요한 정보만 추출하여 제공한다. 예를 들어, videoId만 필요하다면 그것만 추출하여 저장이 가능하다.
- 뷰 로직 단순화:
- 뷰 컨트롤러는 여러 데이터 소스를 직접 다루지 않고, 하나의 ViewModel만 처리하면 된다.
- 비즈니스 로직 분리:
- 데이터를 결합하고 가공하는 로직을 ViewModel에 넣음으로써, 뷰 컨트롤러에서 이러한 로직을 분리할 수 있다.
- 유연성과 확장성:
- 나중에 다른 데이터 소스가 추가되거나 데이터 구조가 변경되어도, ViewModel 내부에서만 변경하면 되므로 뷰 컨트롤러의 변경을 최소화할 수 있다.
import UIKit
protocol CollectionViewTableViewCellDelegate: AnyObject {
func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel:TitlePreviewViewModel)
}
class CollectionViewTableViewCell: UITableViewCell {
// MARK: Variables
static let identifier = "CollectionViewTableViewCell"
weak var delegate: CollectionViewTableViewCellDelegate?
// HomeViewController에서 받은 데이터를 담기 위한 변수 배열
...
}
extension CollectionViewTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
let title = titles[indexPath.row]
guard let titleName = title.original_title ?? title.original_name else { return }
APICaller.shared.getMovie(with: titleName + " trailer") { [weak self] result in
switch result {
case .success(let videoElement):
let title = self?.titles[indexPath.row]
guard let titleOverview = title?.overview else { return }
guard let strongSelf = self else { return }
let viewModel = TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: titleOverview)
self?.delegate?.collectionViewTableViewCellDidTapCell(strongSelf, viewModel: viewModel)
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
- CollectionViewTableViewCellDelegate 프로토콜을 정의하여 셀 탭 이벤트를 처리할 수 있게 했다.
- collectionView(_:didSelectItemAt:)
- 선택된 타이틀의 트레일러 정보를 가져오기 위해 API를 호출하는데, [weak self]를 사용하여 강한 참조 순환을 방지한다.
APICaller.shared.getMovie(with: titleName + " trailer") { [weak self] result in
- 결과 처리
- API 호출이 성공하면, 필요한 정보를 모아 TitlePreviewViewModel을 생성한다.
- 생성된 뷰 모델을 델리게이트 메서드를 통해 상위 뷰 컨트롤러에 전달한다. (상위 뷰 컨트롤러 = HomeViewController)
- API 호출이 실패하면 에러 메시지를 출력한다.
switch result {
case .success(let videoElement):
let title = self?.titles[indexPath.row]
guard let titleOverview = title?.overview else { return }
guard let strongSelf = self else { return }
let viewModel = TitlePreviewViewModel(title: titleName, youtubeView: videoElement, titleOverview: titleOverview)
self?.delegate?.collectionViewTableViewCellDidTapCell(strongSelf, viewModel: viewModel)
case .failure(let error):
print(error.localizedDescription)
}
- 상위 뷰 컨트롤러인 HomeViewController 에서 셀 과 뷰 컨트롤러 사이의 통신 채널을 설정하는 "cell.delegate = self" 코드를 삽입한다.
- 이에 따라 CollectionViewTableViewCellDelegate 프로토콜의 메서드를 구체적으로 구현한다.
import UIKit
...
class HomeViewController: UIViewController {
...
}
// MARK: Extensions
extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CollectionViewTableViewCell.identifier, for: indexPath) as? CollectionViewTableViewCell else { return UITableViewCell() }
cell.delegate = self
...
}
...
}
extension HomeViewController: CollectionViewTableViewCellDelegate {
func collectionViewTableViewCellDidTapCell(_ cell: CollectionViewTableViewCell, viewModel: TitlePreviewViewModel) {
DispatchQueue.main.async { [weak self] in
let vc = TitlePreviewViewController()
vc.configure(with: viewModel)
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
'iOS > UIKIT' 카테고리의 다른 글
검색 결과로 나온 영화 포스터를 누르면 상세페이지로 넘어가기 (0) | 2024.07.10 |
---|---|
랜덤으로 영화 표시하기 (0) | 2024.07.10 |
유튜브에서 제공하는 API를 통해 영화 트레일러 가져오기 (0) | 2024.07.08 |
서치바? 검색창은 어떻게 사용하는가? (0) | 2024.07.05 |
API를 통해 가져온 데이터를 테이블 내에 컬렉션뷰 이미지로 넣기 (0) | 2024.07.05 |