본문 바로가기

Project/MovieClip

상세페이지에서 video, poster 데이터를 UICollectionView로 구현

 

 

✅ DetailViewController에서 video, poster 데이터를 받아 UICollectionView로 설정하는 로직 정리

 

1️⃣ 개요

  • DetailViewController에서 fetchContentDetail()을 통해 API에서 데이터를 가져옴.
  • video, poster 데이터를 enum을 활용해 타입을 구분하여 저장.

⭐️ DetailViewController에서 콘텐츠 유형(movie, tv, people)을 구분
⭐️ MediaTableViewCell에서 미디어 유형(poster, video)을 구분

  • MediaTableViewCell에서 collectionView를 이용해 데이터를 표시.
  • MediaCollectionViewCell에서 video와 poster에 따라 다른 UI 적용.

 

2️⃣ DetailViewController에서 데이터를 받아서 처리하는 흐름

1단계: API를 통해 video, poster 데이터를 가져옴.
2단계: 가져온 데이터를 mediaVideos, mediaPosters 배열에 저장.
3단계: UITableView의 cellForRowAt에서 MediaTableViewCell에 데이터 전달.
4단계: MediaTableViewCell에서 collectionView를 통해 MediaCollectionViewCell에 개별 데이터 전달.
5단계: MediaCollectionViewCell에서 poster와 video에 맞는 UI 설정 적용.

 

 

3️⃣ 단계별 상세 구현

📌 1단계: DetailViewController에서 API 데이터 가져오기 

private func fetchContentDetail() {
    Task {
        do {
            var fetchedDetail: ContentDetail?
            var fetchedGenres: [String] = []
            var castingList: TopBilledCastInfoWelcome?
            var videoInfo: VideoInfoWelcome?
            var posterInfo: PosterInfoWelcome?
            
            switch contentType {
            case .movie:
                let movieDetail = try await NetworkManager.shared.getMovieDetailInfo(movieID: contentID)
                fetchedDetail = .movie(movieDetail)
                fetchedGenres = getGenresFromHomeSection(for: contentID)
                castingList = try await NetworkManager.shared.getMovieCastInfo(contentID: contentID)
                
                videoInfo = try await NetworkManager.shared.getMovieVideoInfo(contentID: contentID)
                posterInfo = try await NetworkManager.shared.getMoviePosterInfo(contentID: contentID)
                
            case .tv:
                let tvDetail = try await NetworkManager.shared.getTVDetailInfo(tvID: contentID)
                fetchedDetail = .tv(tvDetail)
                fetchedGenres = getGenresFromHomeSection(for: contentID)
                castingList = try await NetworkManager.shared.getTVCastInfo(contentID: contentID)
                
                videoInfo = try await NetworkManager.shared.getTvVideoInfo(contentID: contentID)
                posterInfo = try await NetworkManager.shared.getTvPosterInfo(contentID: contentID)
                
            case .people:
                let peopleDetail = try await NetworkManager.shared.getPeopleDetailInfo(peopleID: contentID)
                fetchedDetail = .people(peopleDetail)
                fetchedGenres = getGenresFromHomeSection(for: contentID)
            }
            
            DispatchQueue.main.async {
                self.detailTableHeaderView()
                
                guard let contentDetail = fetchedDetail else { return }
                self.contentDetail = contentDetail
                
                // ✅ 데이터 저장
                switch contentDetail {
                case .movie, .tv:
                    self.mediaVideos = videoInfo?.results ?? []
                    self.mediaPosters = posterInfo?.posters ?? []
                    
                case .people:
                    break
                }
                
                self.detailTableView.reloadData()
            }
        } catch {
            print("❌ 데이터 로드 실패: \(error)")
        }
    }
}

🔥 주요 포인트

  • movie, tv일 경우 mediaVideos와 mediaPosters에 데이터 저장.
  • people은 미디어 정보가 필요 없으므로 데이터 저장 로직 제외.
  • DispatchQueue.main.async에서 UI 업데이트.

 

✔ MediaType (비디오 & 포스터 구분)

✅ MediaTableViewCell에서 비디오와 포스터를 다르게 렌더링할 때 사용.

enum MediaType {
    case poster
    case video
}

 

✔ MediaInfo (비디오 & 포스터 데이터 저장)

MediaCollectionViewCell에서 개별 데이터를 처리할 때 사용.

enum MediaInfo {
    case poster(PosterInfoBackdrop)
    case video(VideoInfoResult)
}

 

 

📌 2단계: cellForRowAt에서 MediaTableViewCell에 데이터 전달

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let sectionType = DetailSection.allCases[indexPath.section]
    
    switch sectionType {
    case .video:
        guard let cell = tableView.dequeueReusableCell(withIdentifier: MediaTableViewCell.reuseIdentifier, for: indexPath) as? MediaTableViewCell else { return UITableViewCell() }
        
        switch contentType {
        case .movie, .tv:
            cell.configure(with: mediaVideos.map { MediaInfo.video($0) }, type: .video) // ✅ 비디오 데이터 전달
        case .people:
            break
        }
        return cell
        
    case .poster:
        guard let cell = tableView.dequeueReusableCell(withIdentifier: MediaTableViewCell.reuseIdentifier, for: indexPath) as? MediaTableViewCell else { return UITableViewCell() }
        
        switch contentType {
        case .movie, .tv:
            cell.configure(with: mediaPosters.map { MediaInfo.poster($0) }, type: .poster) // ✅ 포스터 데이터 전달
        case .people:
            break
        }
        return cell
        
    default:
        return UITableViewCell()
    }
}

🔥 주요 포인트

  • movie, tv의 경우에만 video, poster 데이터를 넘김.
  • map { MediaInfo.video($0) } → VideoInfoResult 배열을 MediaInfo.video 타입으로 변환.
  • map { MediaInfo.poster($0) } → PosterInfoBackdrop 배열을 MediaInfo.poster 타입으로 변환.
  • .people에는 미디어 정보가 필요 없으므로 break.

 

📌 3단계: MediaTableViewCell에서 UICollectionView로 데이터 전달

class MediaTableViewCell: UITableViewCell {
    
    static let reuseIdentifier: String = "MediaTableViewCell"
    
    private var mediaItems: [MediaInfo] = []
    private var mediaType: MediaType = .video
    
    private let mediaCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 10
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.showsHorizontalScrollIndicator = true
        collectionView.backgroundColor = .clear
        return collectionView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupCollectionView()
        configureConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupCollectionView() {
        mediaCollectionView.delegate = self
        mediaCollectionView.dataSource = self
        mediaCollectionView.register(MediaCollectionViewCell.self, forCellWithReuseIdentifier: MediaCollectionViewCell.reuseIdentifier)
    }
    
    func configure(with items: [MediaInfo], type: MediaType) {
        self.mediaItems = items
        self.mediaType = type
        
        if let layout = mediaCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.itemSize = type == .poster ? CGSize(width: 150, height: 250) : CGSize(width: 220, height: 130)
        }
        
        mediaCollectionView.reloadData()
    }
}

 

 

📌 4단계: MediaCollectionViewCell에서 UI 설정 

func configure(with content: MediaInfo) {
    switch content {
    case .video(let video):
        if let key = video.key {
            let thumbnailUrl = URL(string: "https://img.youtube.com/vi/\(key)/hqdefault.jpg")
            thumbnailImageView.sd_setImage(with: thumbnailUrl, completed: nil)
            thumbnailImageView.layer.cornerRadius = 10
        }
    case .poster(let poster):
        if let posterPath = poster.filePath {
            let posterUrl = URL(string: "https://image.tmdb.org/t/p/w500/\(posterPath)")
            thumbnailImageView.sd_setImage(with: posterUrl, completed: nil)
        }
    }
}

 

 

📌 5단계: MediaCollectionViewCell 데이터 전달 

extension MediaTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        switch mediaItems {
        case .poster(let posters):
            return posters.count
        case .video(let videos):
            return videos.count
        case .none:
            return 0
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MediaCollectionViewCell.reuseIdentifier, for: indexPath) as? MediaCollectionViewCell else { return UICollectionViewCell() }
        
        switch mediaItems {
        case .poster(let posters):
            let posterItem = posters[indexPath.item]
            cell.configure(with: .poster(posterItem))
        case .video(let videos):
            let videoItem = videos[indexPath.item]
            cell.configure(with: .video(videoItem))
        case .none:
            break
        }
        return cell
    }
}