[UIKit] Spotify Clone: Playlists

Junyoung Park·2022년 9월 4일
0

UIKit

목록 보기
12/142
post-thumbnail
post-custom-banner

Building Spotify App in Swift 5 & UIKit - Part 12 - Playlists (Xcode 12, 2021, Swift 5) - Build App

Spotify Clone: Playlists

구현 목표

  • 플레이리스트 디테일 뷰
  • 플레이리스트 헤더 뷰 구현
  • 플레이리스트 상세 곡 컬렉션 뷰 재사용
  • 해당 URL 사용하는 공유 기능 구현

구현 태스크

  1. API 호출 이후 패치한 데이터를 통해 UI를 그릴 때 뷰 모델로 받고, 이후 메인 큐로 그리기
  2. RecommendedTrackCollectionViewCell의 레이아웃 및 셀 바인딩 기법 재활용하기
  3. 커스텀 헤더 뷰내의 버튼 클릭 이벤트 감지를 위한 프로토콜 + 델리게이트 패턴 적용하기
  4. 플레이리스트 디테일 뷰 내 공유 기능 버튼 이벤트를 위한 UIActivityViewController 구현하기

핵심 코드

private func fetchData() {
        APICaller.shared.getPlaylistDetails(for: playlist) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let model):
                self.viewModels = model.tracks.items.compactMap({ item in
                    return RecommendedTrackCellViewModel(name: item.track?.name ?? "-", artistName: item.track?.artists.first?.name ?? "-", artworkURL: URL(string: item.track?.album?.images.first?.url ?? ""))
                })
                DispatchQueue.main.async {
                    self.collectionView.reloadData()
                }
                break
            case .failure(let error):
                print(error.localizedDescription)
                break
            }
        }
    }
    
...
let viewModel = viewModels[indexPath.row]
        cell.configure(with: viewModel)
  • 컬렉션 뷰의 각 셀이 사용하는 뷰 모델 데이터 패치 후 UI를 그리는 reloadData() 함수는 메인 큐에서 사용, 메모리 누수 방지를 위한 약한 참조 → 해당 데이터를 셀의 configure을 통해 바인딩
  • 이전의 세 번째 섹션을 그리는 로직과 동일
import UIKit
import SDWebImage

protocol PlaylistHeaderCollectionReusableViewDelegate: AnyObject {
    func playlistHeaderCollectionReusableViewDidTapPlayAll(_ header: PlaylistHeaderCollectionReusableView)
}

final class PlaylistHeaderCollectionReusableView: UICollectionReusableView {
    static let identifier = "PlaylistHeaderCollectionReusableView"
    weak var delegate: PlaylistHeaderCollectionReusableViewDelegate?
    
    private let nameLabel: UILabel = {
        // label
    }()
    
    private let descriptionLabel: UILabel = {
        // label
    }()
    
    private let ownerLabel: UILabel = {
        // label
    }()
    
    private let imageView: UIImageView = {
        // image
    }()
    
    private let playAllButton: UIButton = {
        // button
    }()
    
    ...
    
    override func layoutSubviews() {
        // layout
    }
    
    func configure(with viewModel: PlaylistHeaderViewViewModel) {
        nameLabel.text = viewModel.name
        descriptionLabel.text = viewModel.description
        ownerLabel.text = viewModel.ownerName
        imageView.sd_setImage(with: viewModel.artworkURL, completed: nil)
    }
    
    @objc private func didTapPlayAll() {
        delegate?.playlistHeaderCollectionReusableViewDidTapPlayAll(self)
    }
}
  • 플레이리스트의 헤더 뷰를 담당하는 별도의 커스텀 클래스 구현
  • 플레이리스트 뷰의 버튼 클릭 이벤트 시 구현할 didTapPlayAll은 델리게이트 디자인 패턴으로 구현하기 위한 프로토콜
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionView.elementKindSectionHeader,
              let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: PlaylistHeaderCollectionReusableView.identifier, for: indexPath) as? PlaylistHeaderCollectionReusableView else {
            return UICollectionReusableView()
        }
        let headerViewModel = PlaylistHeaderViewViewModel(name: playlist.name,
                                                          ownerName: playlist.owner.display_name,
                                                          description: playlist.description,
                                                          artworkURL: URL(string: playlist.images.first?.url ?? ""))
        header.configure(with: headerViewModel)
        header.delegate = self
        return header
    }
    
extension PlaylistViewController: PlaylistHeaderCollectionReusableViewDelegate {
    func playlistHeaderCollectionReusableViewDidTapPlayAll(_ header: PlaylistHeaderCollectionReusableView) {
        print("Playing All")
    }
}
  • 컬렉션 뷰의 셀에 데이터 바인딩을 통해 UI를 그리는 것과 마찬가지로 헤더 또한 configure 사용
  • 델리게이트 함수를 사용하는 방법 또한 동일
    @objc private func didTapShare() {
        guard let url = URL(string: playlist.external_urls["spotify"] ?? "") else {
            return
        }
        let vc = UIActivityViewController(activityItems: [url],
                                          applicationActivities: [])
        vc.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
        present(vc, animated: true)
    }
  • 플레이버스트 디테일 뷰의 공유 버튼 클릭 시 발생 이벤트
  • UIActivityViewController를 통해 공유 이벤트 발생

구현 화면

iOS의 공유 기능을 이렇게 구현 가능한지 처음 깨달았다!

profile
JUST DO IT
post-custom-banner

0개의 댓글