[UIKit] Spotify Clone: Player 2

Junyoung Park·2022년 9월 10일
0

UIKit

목록 보기
19/142
post-thumbnail

Building Spotify App in Swift 5 & UIKit - Part 18 - Play Music (Xcode 12, 2021, Swift 5)

Spotify Clone: Player 2

구현 목표

  • 음악 재생 기능 구현

구현 태스크

  1. 델리게이트 패턴 → PlayBackPresenter 함수 사용
  2. 데이터소스 패턴 → PlayerDataSource를 선언한 PlayBackPresenter의 입력 데이터를 PlayerViewController 등에서 사용 가능
  3. AVFoundation → 단일 오디오 트랙 및 여러 개의 오디오 플레이어 큐 사용

핵심 코드

protocol PlayerDataSource: AnyObject {
    var songName: String? { get }
    var subtitle: String? { get }
    var imageURL: URL? { get }
}
  • 현재 재생 중인 음원의 정보를 저장하기 위한 데이터소스 프로토콜
private var track: AudioTrack?
    private var tracks = [AudioTrack]()
    var player: AVPlayer?
    var playerQueue: AVQueuePlayer?
    var currentTrack: AudioTrack? {
        if let track = track, tracks.isEmpty {
            return track
        } else if let player = playerQueue, !tracks.isEmpty {
            guard let item = player.currentItem else { return nil }
            let items = player.items()
            guard let index = items.firstIndex(of: item) else { return nil }
            return tracks[index]
        }
        return nil
    }
  • PlayBackPresenter 서비스 클래스의 선언 변수
  • 재생될 음원을 입력받고 연산 프로퍼티를 통해 현재 시점의 음원을 리턴하는 currentTrack 변수
func startPlayback(from viewController: UIViewController, track: AudioTrack) {
        let vc = PlayerViewController()
        self.track = track
        guard let urlString = currentTrack?.preview_url else { return }
        guard let url = URL(string: urlString) else {
            return
        }
        player = AVPlayer(url: url)
        player?.volume = 0.0
        self.track = track
        self.tracks = []
        vc.title = track.name
        vc.dataSource = self
        vc.delegate = self
        viewController.present(UINavigationController(rootViewController: vc), animated: true) { [weak self] in
            guard let self = self else { return }
            self.player?.play()
        }
    }
  • preview_url 변수는 해당 음원의 미리 듣기를 제공하는 주소
  • 음원 종류(단수/복수)에 따라 큐에 넣어서 음원을 재생할지 결정
  • PlayerViewController를 현 시점에 클릭한 뷰 컨트롤러에 모달로 준 뒤 띄우기 전 데이터소스 및 델리게이트를 selfPlayBackPresenter로 등록
  • PlayerViewController의 해당 프로토콜 정보를 바탕으로 입력받은 재생 함수, 음원 데이터를 사용 가능
extension PlaybackPresenter: PlayerDataSource {
    var songName: String? {
        return currentTrack?.name
    }
    
    var subtitle: String? {
        return currentTrack?.artists.first?.name
    }
    
    var imageURL: URL? {
        if let urlString = currentTrack?.album?.images.first?.url {
            return URL(string: urlString)
        } else if let urlString = currentTrack?.artists.first?.images?.first?.url {
            return URL(string: urlString)
        } else {
            return nil
        }
    }
}
  • PlayerDataSource 프로토콜에 따라 리턴할 연산 프로퍼티
extension PlaybackPresenter: PlayerViewControllerDelegate {
    func didTapPlayPause() {
        if let player = player {
            if player.timeControlStatus == .playing {
                player.pause()
            } else if player.timeControlStatus == .paused {
                player.play()
            }
        } else if let player = playerQueue {
            if player.timeControlStatus == .playing {
                player.pause()
            } else if player.timeControlStatus == .paused {
                player.play()
            }
        }
    }
    
    func didTapForward() {
        if tracks.isEmpty {
            player?.pause()
            player?.play()
        } else if let player = playerQueue {
            player.advanceToNextItem()
        }
    }
    
    func didTapBackward() {
        if tracks.isEmpty {
            player?.pause()
        } else if let firstItem = playerQueue?.items().first {
            playerQueue?.pause()
            playerQueue?.removeAllItems()
            playerQueue = AVQueuePlayer(items: [firstItem])
            playerQueue?.play()
            playerQueue?.volume = 0
        }
    }
    
    func didSlideSlider(_ value: Float) {
        player?.volume = value
    }
}
  • PlayerViewControllerDelegate 프로토콜에 따라 PlayBackPresenter 클래스 내에서 선언된 함수
  • PlayerViewControllerDelegateweak var로 가지고 있는 PlayerViewController를 모달로 띄우는 PlayBackPresenter 클래스 단계에서 self로 데이터 소스, 델리게이트를 등록 → PlayerViewController의 델리게이트를 가지고 있는 PlayerControlsView의 이벤트 발생 시 PlayBackPresenter 클래스의 함수를 사용 가능
extension PlayerViewController: PlayerControlsViewDelegate {
    func playerControlsView(_ playerControlsView: PlayerControlsView, didSlideSlider value: Float) {
        delegate?.didSlideSlider(value)
    }
    func playerControlsViewDidTapPlayPauseButton(_ playerControllsView: PlayerControlsView) {
        delegate?.didTapPlayPause()
    }
    
    func playerControlsViewDidTapForwardButton(_ playerControllsView: PlayerControlsView) {
        delegate?.didTapForward()
    }
    
    func playerControlsViewDidTapBackwardButton(_ playerControllsView: PlayerControlsView) {
        delegate?.didTapBackward()
    }
}
  • 위 델리게이트는 PlayerViewController가 모달 뷰로 띄워질 때 PlayBackPresenter 클래스가 self로 델리게이트 등록한 뒤의 델리게이트로 PlayerControlsViewDelegatePlayerControlsView 이벤트를 감지한 뒤 자신이 갖고 있는 PlayerBackPresenter 함수를 사용한 것

구현 화면

두 개 이상의 델리게이트, 데이터소스가 등장해 연결고리가 쌓이니 각 클래스 간의 연관관계를 파악하는 데 난이도가 높아진다. 델리게이트 패턴을 대체할 수 있는 rx 등이 인기를 얻은 까닭을 왜인지 모르게 알 것 같다.

profile
JUST DO IT

0개의 댓글