Building Spotify App in Swift 5 & UIKit - Part 18 - Play Music (Xcode 12, 2021, Swift 5)
PlayBackPresenter
함수 사용PlayerDataSource
를 선언한 PlayBackPresenter
의 입력 데이터를 PlayerViewController
등에서 사용 가능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
를 현 시점에 클릭한 뷰 컨트롤러에 모달로 준 뒤 띄우기 전 데이터소스 및 델리게이트를 self
즉 PlayBackPresenter
로 등록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
클래스 내에서 선언된 함수PlayerViewControllerDelegate
를 weak 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
로 델리게이트 등록한 뒤의 델리게이트로 PlayerControlsViewDelegate
로 PlayerControlsView
이벤트를 감지한 뒤 자신이 갖고 있는 PlayerBackPresenter
함수를 사용한 것두 개 이상의 델리게이트, 데이터소스가 등장해 연결고리가 쌓이니 각 클래스 간의 연관관계를 파악하는 데 난이도가 높아진다. 델리게이트 패턴을 대체할 수 있는
rx
등이 인기를 얻은 까닭을 왜인지 모르게 알 것 같다.