[iOS/부스트 코스] iOS 앱 프로그래밍: Music Player(3) - 기능 구현
안녕하세요. 개발하는 정주입니다.
오늘은 Music Player(3) - 기능 구현에 대해 포스팅하려고 합니다.
구현해야 하는 기능 종류
1. 버튼을 누르면 음원이 play/pause가 되고 아이콘이 바뀐다.
2. 음원 진행 사항에 따라 Slider와 타임 레이블의 텍스트가 변경된다.
3. Slider를 움직이면 타임 레이블 텍스트가 변경된다.
4. Slider를 움직이는 동안은 음원이 계속 재생된다.
5. Slider를 놓으면 해당 지점으로 음원이 점프된다.
6. 재생이 끝나면 play 버튼으로 변경되고 Slider와 타임 레이블이 초기화된다.
음원 기능
iOS에서 음원은 AVAudioPlayer 클래스를 이용합니다.
guard let soundAssets:NSDataAsset = NSDataAsset(name: "sound") else {
print("음원 파일 애셋을 가져올 수 없습니다.")
return
}
do {
try self.player = AVAudioPlayer(data: soundAssets.data)
self.player.delegate = self
} catch let error as NSError {
print("플레이어 초기화 실패")
print("code: \(error.code) / message: \(error.localizedDescription)")
}
NSDataAsset을 이용해 sound 에셋을 가져옵니다. AVAudioPlayer 클래스에 에셋 데이터를 넘겨서 해당 mp3 파일을 가진 객체를 생성합니다. 음원이 끝났다는 것을 알기 위해 delegate를 설정합니다. AVAudioPlayerDelegate 프로토콜을 채택합니다.
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
self.playPuaseButton.isSelected = false
self.progressSlider.value = 0
self.updateTimeLabelText(time: 0)
self.invalidateTimer()
}
음원이 끝났다는 것은 AVAudioPlayerDelegate 프로토콜의 audioPlayerDidFinishPlaying()을 이용하면 됩니다.
음원이 끝나면 버튼과 Slider, 레이블을 처음의 상태로 돌리고 타이머를 종료시킵니다.
참고 링크
AVAudioPlayer 애플 공식 문서 https://developer.apple.com/documentation/avfaudio/avaudioplayer
Play/Pause 버튼
Play/Pause 버튼의 핵심은 음원의 재생 여부를 컨트롤하는 것입니다.
처음에는 Play 버튼으로 시작하며 Play 버튼을 누르면 음원이 재생되고 Pause 이미지로 변경됩니다.
Pause 버튼을 누르면 음원이 정지하며 Play 이미지로 변경됩니다. 이때 Slider와 Time Label의 갱신 여부도 재생 여부와 연동됩니다.
@IBAction func touchUpPlayPauseButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if sender.isSelected {
self.player?.play()
self.makeAndFireTimer()
} else {
self.player?.pause()
self.invalidateTimer()
}
}
playPuaseButton에 연결된 IBAction입니다. 화면 구성 포스팅에서 보셨듯이 이미지 변경 기준을 selected로 정했기 때문에 UIButton의 selected 속성을 변경하여 설정된 이미지를 교체합니다.
UIButton이 선택된 상태라면 음원을 재생, 타이머를 시작하고 미선택 상태라면 음원을 pause, 타이머를 종료합니다.
타이머
iOS에서 타이머는 Timer 클래스를 이용합니다. 음원이 재생하면 음원의 길이만큼 타이머를 생성합니다. 타이머가 진행되는 값에 의해 Label의 텍스트를 변경해줍니다.
self.timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [unowned self] (timer: Timer) in
if self.progressSlider.isTracking { return }
self.updateTimeLabelText(time: self.player.currentTime)
self.progressSlider.value = Float(self.player.currentTime)
})
타이머를 생성하는 코드입니다. Label의 형식은 00:00:00입니다. 0.01초부터 표시가 되므로 withTimeInterval 값으로 0.01을 줍니다.
음원이 끝날 때까지 반복해야 하므로 repeats는 true로 설정합니다. ARC를 위한 unowned self로 timer를 정의하였습니다.
0.01초마다 label과 slider를 업데이트합니다.
Slider가 Tracking(터치)되고 있는 동안은 다른 메서드에서 label을 업데이트하므로 타이머에서는 업데이트하지 않도록 합니다.
func invalidateTimer() {
self.timer.invalidate()
self.timer = nil
}
타이머를 종료할 땐 invalidate를 해주고 nil로 설정하여 메모리 회수가 되도록 해줍니다.
참고 링크
Timer 애플 공식 문서 https://developer.apple.com/documentation/foundation/timer
weak와 unowned의 차이 https://eastjohntech.blogspot.com/2019/12/weak-unowned.html
isTracking 애플 공식 문서 https://developer.apple.com/documentation/uikit/uiscrollview/1619413-istracking
Time Label 업데이트
타이머가 동작하거나 Slider를 움직일 때마다 Label의 텍스트가 업데이트됩니다.
func updateTimeLabelText(time: TimeInterval) {
let minute: Int = Int(time / 60)
//Returns the remainder of this value divided by the given value using truncating division.
let second: Int = Int(time.truncatingRemainder(dividingBy: 60))
let milisecond: Int = Int(time.truncatingRemainder(dividingBy: 1) * 100)
let timeText: String = String(format: "%02ld:%02ld:%02ld", minute, second, milisecond)
self.timeLabel.text = timeText
}
여기서 타임은 "self.player.currentTime"을 받습니다. currentTime은 재생 시간을 초 단위로 줍니다.
따라서 minute 값은 60을 나눈 값이고 second는 60을 나눈 나머지 값입니다.
이 나머지를 구하기 위해 truncatingremainder()를 사용합니다. Swift에서 % 연산은 Int 끼리만 가능하기 때문에 소수점이 있는 Double이나 Float의 나머지 연산은 truncatingremainder()를 이용해야 합니다.
분, 초, 밀리초를 각각 구한 후 String으로 변환하여 label의 text로 설정합니다.
참고 링크
truncatingremainder()를 사용하는 이유 https://minzombie.github.io/swift/truncatingRemainder/
Slider
iOS에서 Seek Bar는 UISlider 클래스를 사용합니다.
self.progressSlider.minimumValue = 0
self.progressSlider.maximumValue = Float(self.player.duration)
self.progressSlider.value = Float(self.player.currentTime)
앱을 시작할 때 Slider를 초기화해줍니다. 예제 코드를 max -> min -> value 순으로 작성했지만 저는 가독성을 위해 min -> max -> value 순으로 작성하였습니다.
@IBAction func sliderValueChanged(_ sender: UISlider) {
let currentTime = TimeInterval(sender.value)
self.updateTimeLabelText(time: currentTime)
//Slider를 움직이는 동안은 음원이 계속 재생된다.
if sender.isTracking { return }
self.player.currentTime = currentTime
}
Slider는 값이 변할 때마다 이벤트 처리를 해줘야 하기 때문에 Value Changed IBAction을 연결합니다. 그러면 Slider의 값이 변할 때마다 액션 메서드가 호출될 것입니다.
Slider의 값을 currentTime으로 설정하고 Time Label을 업데이트합니다. 단, Slider를 터치해서 움직이는 동안은 음원이 점프하면 안 되므로 조건 처리를 합니다.
마무리 잡담
오늘은 기능 구현을 해보았습니다. 다음 시간에는 오토 레이아웃에 대해 알아보겠습니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.