iOS/UIKIT

webkit을 통해 유튜브 영상의 주소로 영상 틀어보기

밤새는 탐험가89 2024. 7. 8. 21:39

 

 

구현 내용

  • 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을 사용하는 주요 이유는 다음과 같다. 

  1. 다중 데이터 소스 통합:
    • Title 모델에서 영화/TV 쇼의 제목과 개요를 가져온다.
    • YouTube API 응답에서 관련 비디오 정보를 가져오는데,  이 두 가지 다른 데이터 소스의 정보를 하나의 ViewModel로 통합하여 사용하기 위함이다. 
  2. 데이터 변환 및 가공:
    • YouTube API 응답에서 필요한 정보만 추출하여 제공한다. 예를 들어, videoId만 필요하다면 그것만 추출하여 저장이 가능하다.
  3. 뷰 로직 단순화:
    • 뷰 컨트롤러는 여러 데이터 소스를 직접 다루지 않고, 하나의 ViewModel만 처리하면 된다.
  4. 비즈니스 로직 분리:
    • 데이터를 결합하고 가공하는 로직을 ViewModel에 넣음으로써, 뷰 컨트롤러에서 이러한 로직을 분리할 수 있다.
  5. 유연성과 확장성:
    • 나중에 다른 데이터 소스가 추가되거나 데이터 구조가 변경되어도, 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)
        }
    }
}