#1 利用 page control,segmented control,button & gesture 製作音樂播放器
目的:
1. 熟悉page control,segmented control,button & gesture UI元件
2. 利用segmented control 控制 scrollView滑動
3. 使用 AVFoundation播放mp3
4. 使用slider控制播放進度,抓取播放進度
5. 運用Timer時時更新播放時間
先看成品吧:
好吧進入實作部分~
介面創建:
剩下的四個,播放起始時間、播放終點時間、播放歌曲名稱&歌手及現在播放第幾首歌曲,在拉下去眼花撩亂了~
再繼續下去之前一定要先準備音樂素材&歌手圖片,不然code寫到一半找素材會打斷思緒
確認資源:
在你拖進資源後一定要確認這個資源是否在Copy Bundle Resources,不然等等會撈不到資源
創建資源物件:
新建一個swift檔案來裝資料用,創建一個專輯的結構 Album 並在下方的創建剛剛拉入的歌曲與歌手名稱,至於 getAlbum 這個多型是等等畫面要來取資料用的
(小聲說這個類別方法我覺得寫得不好)
struct Album {
let singer: String
let singTitle: String
}
class TotalAlbum{
// 使用歌名抓取 歌手專輯
func getAlbum(singer: String) -> (album: [Album], index: Int) {
switch singer{
case "陳勢安":
return (album: 陳勢安, index: 0)
case "周杰倫":
return (album: 周杰倫, index: 1)
case "黃鴻升":
return (album: 黃鴻升, index: 2)
default:
return (album: 陳勢安, index: 0)
}
}
// 用index抓取 歌手專輯
func getAlbum(index: Int) -> [Album] {
switch index{
case 0:
return 陳勢安
case 1:
return 周杰倫
case 2:
return 黃鴻升
default:
return 陳勢安
}
}
let 陳勢安: [Album] = [
Album(singer: "陳勢安", singTitle: "天后"),
Album(singer: "陳勢安", singTitle: "勢在必行")
]
let 周杰倫: [Album] = [
Album(singer: "周杰倫", singTitle: "一路向北"),
Album(singer: "周杰倫", singTitle: "給我一首歌的時間"),
Album(singer: "周杰倫", singTitle: "告白氣球")
]
let 黃鴻升: [Album] = [
Album(singer: "黃鴻升", singTitle: "地球上最浪漫的一首歌"),
Album(singer: "黃鴻升", singTitle: "有感情歌"),
Album(singer: "黃鴻升", singTitle: "超有感")
]
}
介面元件初始化:
override func viewDidLoad() {
super.viewDidLoad()
// 預設播放
theAlbum = totalAlbum.陳勢安
signer = "陳勢安"
// 左右切換按鈕
// 設置按鈕在 normal 狀態下的文字
leftBtn.setTitle("", for: .normal)
// 將圖片更改
let imageLeft = UIImage(systemName: "backward.end.fill")
// 將圖片放進btn
leftBtn.setImage(imageLeft, for: .normal)
// 設置按鈕在 normal 狀態下的文字
rightBtn.setTitle("", for: .normal)
// 將圖片更改
let imageRight = UIImage(systemName: "forward.end.fill")
// 將圖片放進btn
rightBtn.setImage(imageRight, for: .normal)
// 一開始播放按鈕為停止
setPlayingImage(icon: .stop)
// UISlider
// UISlider 滑桿按鈕右邊 尚未填滿的顏色
timeSlider.maximumTrackTintColor = UIColor.lightGray
// UISlider 滑桿按鈕左邊 已填滿的顏色
timeSlider.minimumTrackTintColor = UIColor.white
// UISlider 的最小值
timeSlider.minimumValue = 0
// UISlider 的最大值
timeSlider.maximumValue = 1
// UISlider 預設值
timeSlider.value = 0
// 一開始不顯示時間
playerCurrentTimeLable.text = ""
playerDurationLable.text = ""
// signerImageView 導圓角
signerImageView.layer.cornerRadius = 10
updateUI()
}
我習慣將介面都先拉好定位,再將按鈕樣式,歌手資料初始化,這樣做可以確保我每個元件都有初始化到
播放:
音樂播放器一定要先能發出聲音啦~
這裡有一個比較特別的代理 AVAudioPlayerDelegate ,這個代理我們等會要抓取播放時的音樂長度,還有播放進度等等的方法
呼叫這個函式裡有個 Album
它是我們上面提到的結構,裡面包含了歌手名與歌曲名 裡面還有一個 timer 他是用來不斷更新播放進度的秒數用的。
class ViewController: UIViewController, AVAudioPlayerDelegate {
...
}
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
播放計時器:
這傢伙(計時器)本來應該在這裡的,timer會在我們播放音樂時啟動,停止時關掉
// 計時器
var timer: Timer?
override func viewDidLoad() {
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [self] timer in
// 在這裡放置你想要每秒執行一次的程式碼
timeSlider.value = getPlaybackProgress()
// 播放進度時間
let currentTime = FormatTime.formatTime(getCurrentTime())
playerCurrentTimeLable.text = currentTime
}
}
但發生了一點小插曲,app開始播放音樂後使用 timer.invalidate()
來停止計時器,看似美好,再次按下播放鍵同時啟動計時器timer?.fire()
,疑!奇怪怎麼時間跟slider都不動了,不信邪的再試一次
override func viewDidLoad() {
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [self] timer in
print("我有執行嗎")
}
}
還是沒反應???
看了一下官方的文件,沒看出端倪,所以在Closure 裡的程式碼在被啟用後Closure裡的內容就重新來過了?
好吧!你這個樣子我只好來點手段,既然Closure裡的東西會遺失,我停止時就直間把你丟了,啟動時再重新給你我的內容(很暴力?),為了避免重複賦予time造成一些奇怪的事情,先判定前一個timer是不是空的
// 計時器
var timer: Timer?
...
// 計時器每秒觸發的方法
func timerFired() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [self] timer in
// 在這裡放置你想要每秒執行一次的程式碼
timeSlider.value = getPlaybackProgress()
// 播放進度時間
let currentTime = FormatTime.formatTime(getCurrentTime())
playerCurrentTimeLable.text = currentTime
}
}
}
// MARK: 計時器
func timerStop() {
timer?.invalidate()
timer = nil
}
終於解決timer的問題
AVAudioPlayerDelegate 代理:
還記得前面的播放按鈕內容吧,裡面有個代理就是它,他可以給我們好多個代理,包含音樂播放完成、音樂播放解碼異常、中斷播放等等。
我這裡想要將音樂播放完成後自動執行下一首
// MARK: AVAudioPlayerDelegate 的代理
// 實作 AVAudioPlayerDelegate 方法,處理播放完成事件等
@objc func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
// 音樂播放完成後的處理
nextMusic()
}
// 下一首歌
func nextMusic() {
index = (index + 1) % theAlbum!.count
// 播放音樂
playerMusic(alblum: theAlbum![index])
updateUI()
}
我沒有一一的測試所有方法,這裡記錄一下(下次有需求再測試吧?)
音樂播放進度:
這裡為了讓我的 Slider 不用一直更改他的 MAX 值,我把Slider的值設定於 0–1之間,所以這裡我留下了 setPlaybackProgress 這個多型,想用秒數或0–1控制播放進度都可以,(擁抱chatGPT老師的大腿吧)。
override func viewDidLoad() {
...
// UISlider 的最小值
timeSlider.minimumValue = 0
// UISlider 的最大值
timeSlider.maximumValue = 1
// UISlider 預設值
timeSlider.value = 0
}
// MARK: 自定義方法 - 抓取音樂播放時間相關
// 設置音樂播放進度(以0-1為單位)
func setPlaybackProgress(progress: Float) {
guard let audioPlayer = audioPlayer else {
return
}
let duration = audioPlayer.duration
let currentTime = TimeInterval(progress) * duration
audioPlayer.currentTime = currentTime
}
// 設置音樂播放進度(以秒為單位)
func setPlaybackProgress(seconds: TimeInterval) {
guard let audioPlayer = audioPlayer else {
return
}
audioPlayer.currentTime = seconds
}
// 取得音樂播放進度
func getPlaybackProgress() -> Float {
guard let audioPlayer = audioPlayer else {
return 0.0
}
return Float(audioPlayer.currentTime / audioPlayer.duration)
}
// 取得音樂總長度(以秒為單位)
func getMusicDuration() -> TimeInterval {
guard let audioPlayer = audioPlayer else {
return 0.0
}
return audioPlayer.duration
}
// 取得當前播放時間(秒)
func getCurrentTime() -> Double {
return audioPlayer!.currentTime
}
使用 getCurrentTime() 抓取現在播放的秒數,回傳值直接放入我們的 playerCurrentTimeLable
// 計時器每秒觸發的方法
func timerFired() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [self] timer in
...
let currentTime = FormatTime.formatTime(getCurrentTime())
playerCurrentTimeLable.text = currentTime
}
}
} // 播放音樂
func playerMusic(alblum: Album){
if let filePath = Bundle.main.path(forResource: alblum.singTitle, ofType: "mp3") {
let fileURL = URL(fileURLWithPath: filePath)
do {
...
// 顯示播放總時
playerDurationLable.text = FormatTime.formatTime(getMusicDuration())
} catch {
print("Error loading audio file: \(error)")
}
}
使用 getMusicDuration() 抓取音樂長度,回傳值放入我們的 playerDurationLable 音樂長度不需要時時更新,所以放在播放鈕上面,只要切換音樂就會更新一次
// 播放音樂
func playerMusic(alblum: Album){
if let filePath = Bundle.main.path(forResource: alblum.singTitle, ofType: "mp3") {
let fileURL = URL(fileURLWithPath: filePath)
do {
...
// 顯示播放總時
playerDurationLable.text = FormatTime.formatTime(getMusicDuration())
} catch {
print("Error loading audio file: \(error)")
}
}
}
運用 segmented Control 切換歌手:
這裡的內容必須與 Album
內的歌手一樣,不然找不到就切換不了歌手囉~
將歌手名字傳入 TotalAlbum 的 getAlbum(singer: String) -> (album: [Album], index: Int) 方法,抓取此歌手的所有歌曲與index(這順序是要給 page Control 的)。
// 切換歌手
@IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {
let selectSinger = sender.titleForSegment(at: sender.selectedSegmentIndex)
// 將歌名放入播放清單
let album = totalAlbum.getAlbum(singer: selectSinger!)
theAlbum = album.album
// 將歌手放入全域變數(會使用到)
signer = selectSinger!
// 從第一首開始
index = 0
// 點點等於現在選的歌手index
pageControl.currentPage = album.index
// 播放音樂
playerMusic(alblum: theAlbum![index])
updateUI()
}
class TotalAlbum{
struct Album {
let singer: String
let singTitle: String
}
// 使用歌名抓取 歌手專輯
func getAlbum(singer: String) -> (album: [Album], index: Int) {
switch singer{
case "陳勢安":
return (album: 陳勢安, index: 0)
case "周杰倫":
return (album: 周杰倫, index: 1)
case "黃鴻升":
return (album: 黃鴻升, index: 2)
default:
return (album: 陳勢安, index: 0)
}
}
...
}
使用 pageControl (點點)更換歌手:
因為要連動 segmented Control ,所以要更改它的 index
// pageControl 更改時
@IBAction func changePage(_ sender: UIPageControl) {
let album = totalAlbum.getAlbum(index: sender.currentPage)
theAlbum = album
// 從第一首開始
index = 0
// 點點改動時也更改 segmentedControl
segmentedControl.selectedSegmentIndex = sender.currentPage
// 播放音樂
playerMusic(alblum: theAlbum![index])
updateUI()
}
第一次在medium寫文章,很多功能都不熟悉,我嚴重懷疑我打Medium花的時間可能比寫App的時間還多,不過截圖過程中發現有更多可以寫更好的地方,後續有時間會再回來整理一下Code,這篇就先到這裡啦~😃
附上gitHub: