Swift 3 :基于 AVAudioPlayer 的简单音乐播放器

学习ios以来差不多接近两个月了,作为一个刚入行的菜鸡终于鼓起勇气写博客并发布出来,本周课程讲到了ios多媒体应用关于音频播放这部分(本菜还在读大学走的移动端ios方向= =),课下作业老师让做个基于AVAudioPlayer的音乐播放器,问题不大,比较简单,想给自己加点难度,最近swift这么火,干脆用swift写个吧!于是逼着自己一点点的边做边啃swift语法,花了差不多3天的时间,粗制滥造 = =!的搞出来个简单版的音乐播放器(希望大家多多指点,供跟我一样刚入行的朋友互相交流,互相学习),如图:


效果图.gif

1.界面

最初想的是该怎么入门,毕竟swift认识我,我不认识它啊!还是去世界上最大的同性交友网站GitHub上去看看有没有关于swift播放器的demo把,从学习别人的代码来入门,结果转了一大圈都没有找到合适的,不过发现了个有四百个star的提供炫酷手势界面壳子的项目,但仅仅是个壳子而已,好吧我的界面就用它了,对了说到界面,他界面用的是StoryBoard还用到了个新控件叫ContainerView,ContainerView是用来在一个视图控制器上添加子视图控制器的,他这样做的好处就是可以让迷你播放栏一只处于界面最上方,点击tabbar切换界面的时候,只需要将ContainerView里的子视图控制器更换即可具体用法大家可以自行简书

2.获取音乐及相关信息,封装音乐播放类

界面有了,使用AVAudioPlayer根据本地音乐的路径进行音乐播放,在这里我封装了个方法用来获取本地文件夹myMusic里所有歌曲路径以及作家,封面,歌曲名等。为了做出上一曲下一曲功能我将每首歌曲都编了号。

 static func getALL()->Array<Music>{
        if musicArry == nil {
            var musicArryList=Array<Music>()
            var fileArry:[String]?
            var num:Int=0;
            let path=Bundle.main.path(forResource:"mymusic", ofType: nil)
            do{
                try fileArry=FileManager.default.contentsOfDirectory(atPath: path!)
            }
            catch{
                print("error")
            }
            for n in fileArry! {
                let singlePath=path!+"/"+n
                let avURLAsset = AVURLAsset(url: URL.init(fileURLWithPath: singlePath))
                let musicModel:Music = Music()
                
                for i in avURLAsset.availableMetadataFormats {
                    
                    for j in avURLAsset.metadata(forFormat: i) {
                        //歌曲名
                        if j.commonKey == "title"{
                            musicModel.musicName = j.value as? String
                            
                        }//封面图片
                        if j.commonKey == "artwork"{
                            musicModel.musicimg=j.value as? Data// 这里是个坑坑T T
                        }//专辑名
                        if j.commonKey == "albumName"{
                            musicModel.musicAlbum=j.value as? String
                        }
                        //歌手
                        if j.commonKey == "artist"{
                            musicModel.musicAuthor=j.value as? String
                        }
                    }
                    
                }
                musicModel.musicURL=URL.init(fileURLWithPath: singlePath)
                num += 1
                musicModel.musicNum=num;
                musicArryList.append(musicModel)
            }
            musicArry=musicArryList
            return musicArry!

        }else{
            return musicArry!
        }
    }

一个音乐播放器每次播放都只能放一首歌,于是我将其封装成了一个AudioPlayer单例类,其实也不能算单例,应该叫工具类比较合适。

import UIKit
import AVFoundation
final class AudioPlayer: NSObject {
    private static var instance: AVAudioPlayer? = nil //static 直到被销毁 全局存在
    private static var activeMusic:Music?=nil
    private static var isRandomPlay=false
    static func share(model:Music) -> Bool {
            do{
                try instance = AVAudioPlayer(contentsOf: model.musicURL!)
            }
            catch{
                instance=nil;
                print("error")
                return false;
            }
            instance?.play()
            activeMusic=model
            return true
    }
    //停止
    static func stop(){
        instance?.stop()
    }
    //播放
    static func play()->Bool{
        if (instance?.isPlaying)! {
            instance?.pause()
            return false
        }else{
            instance?.play()
            return true
        }
    }
    //暂停
    static func pause(){
        instance?.pause()
    }
    //下一曲
    static func nextsong( num:Int)->Bool{
        var num = num
        var musicArry:Array<Music>!
        musicArry=Music.getALL()
        if isRandomPlay{
           num = Int(arc4random_uniform(UInt32(musicArry.count-1)))
        }
        if(share(model: musicArry[num])){
            return true
        }else{
            return false
        }
    }
    //上一曲
    static func prevsong(num:Int)->Bool{
        var num = num
        var musicArry:Array<Music>!
        musicArry=Music.getALL()
        if isRandomPlay{
            num = Int(arc4random_uniform(UInt32(musicArry.count-1)))
        }
        if(share(model: musicArry[num])){
            return true
        }else{
            return false
        }
    }
    //声音控制
    static func voice(num:Float){
        instance?.volume=num
    }
    //进度条相关
    static func progress()->Double{
        return (instance?.currentTime)!/(instance?.duration)!
    }
    static func musicDuration()->Double{
        return (instance?.duration)!
    }
    
    static func currentTime()->Double{
        return (instance?.currentTime)!
    }
 
    //当前播放的音乐
    static func activeSong()->Music?{
        return activeMusic
    }
    //是否在播放音乐
    static func isPlaying()->Bool{
        return (instance?.isPlaying)!
    }
    //随机播放
    static func musicRandomPlay()->Bool{
        if  isRandomPlay==false{
            isRandomPlay=true
            return isRandomPlay
        }else{
            isRandomPlay=false
            return isRandomPlay
        }
    }
    
}

3.音乐播放

所有歌曲及其信息都能拿到了,展示在tableView上我就不用多说,播放音乐也只需要在tableView的相应点击方法里使用AudioPlayer类的share方法将对应的歌曲model传入即可,这里有个如何让miniplayer展示所点击歌曲的问题,我是将tableView放进ContainerView,tableView相当于主视图的子视图,而miniplayer是在主视图里,这里就要用到子视图向父视图回传值的方法,在这里我是采用的闭包回传,其实也不需要传什么值,只需要让主视图知道子视图被点击了即可,因为我可以通过AudioPlayer类来得到当前播放的歌曲信息。swift里的闭包传值在语法上和oc的区别也是很大,花了我不少时间- -
子视图里:

var musicPlayByTableViewCell: ((Int) -> Void)//声明
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        musicPlayByTableViewCell?(indexPath.row)
        tableView.deselectRow(at: indexPath, animated: true)//取消被选中状态
    }

主视图里:

 self.musicTalbleVC.musicPlayByTableViewCell={
          [weak self]  musicNum in self?.musicModel=self?.musicArry[musicNum]
            self?.nowNum=musicNum
            self?.miniPlayerWork()
        }

3.1界面功能及相关细节

音乐播放时的界面也就是几个button和常见的视图控件,说说进度条,AVAudioPlayer类提供了当前进度,以及总时长,所以我是用定时器每一秒获取一次当前进度的方法,再当前进度除以总进度使用即可,上下曲因为我在获取所有歌曲时就将每首歌曲编了号,上下曲就只需要将编号加一或减一就可以了,包括随机播放也只需要使用随机函数随机总歌曲编号就行了,还有些界面上的细节,在详细歌曲播放界面点击了暂停,回到主视图时miniplayer的图标及相关信息也应该与其同步哦。tableView列表上也将当前播放的歌曲进行了高亮展示(加了歌活跃标记),在这里,我在musicmodel里添加了IsActive属性,并在tableView的Cell里进行了判断,如果cell展示的歌曲是当前正在播放的歌曲isActive为true展示活跃标记。还有音乐播放完毕后需要自动播放下一曲哦,这里我是用的定时器控件,读取歌曲进度进度完了就播放下一曲(相当点击了一次下一曲按钮)

3.12tabBar控制界面

在storyBoard上是直接拖的TabBar,和直接用tabBarController大不一样,点击不同的图标响应不同的事件ContainerView里的视图控制器也就相应的变化,关于如何改变ContainerView里的视图控制器我也是找了好久,还比较复杂,但确实ContainerView很好用啊!
ContainerView:更改视图控制器

    func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        if item.tag == 1 {
            let newController = self.musicTalbleVC!
            let oldController = childViewControllers.last!
            if newController != oldController {
              //  self.setStatusBarBackgroundColor(color: UIColor.white)
                oldController.willMove(toParentViewController: nil)
                addChildViewController(newController)
                newController.view.frame = oldController.view.frame
                //isAnimating = true
                transition(from: oldController, to: newController, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromLeft, animations: nil, completion: { (finished) -> Void in
                    oldController.removeFromParentViewController()
                    newController.didMove(toParentViewController: self)                //self.isAnimating = false
                })
            }
        }else{
            let newController = self.moreVC!
            let oldController = childViewControllers.last!
             if newController != oldController {
                self.setStatusBarBackgroundColor(color: UIColor.clear)
                oldController.willMove(toParentViewController: nil)
                addChildViewController(newController)
                newController.view.frame = oldController.view.frame
                transition(from: oldController, to: newController, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromRight, animations: nil, completion: { (finished) -> Void in
                    oldController.removeFromParentViewController()
                    newController.didMove(toParentViewController: self)                //self.isAnimating = false
                })
            }
        }
    }

3.13音乐的后台播放以及锁屏和上拉栏展示音乐信息

音乐的后台播放,因为我还从为接触过应用后台运行这一块,所以我也只是参照网上的代码将功能做出来了而已,还有不完善的地方,后台播放中断(如打电话)后不会恢复播放。上拉栏展示音乐信息以及之中的按钮触发方法,都是系统封装好了的 直接用就是了。
话说很多警告呀!

    func setBackground() {
        //大标题 - 小标题  - 歌曲总时长 - 歌曲当前播放时长 - 封面
        self.musicModel=AudioPlayer.activeSong()
        var settings = [MPMediaItemPropertyTitle: self.musicModel?.musicName,
                        MPMediaItemPropertyArtist: self.musicModel?.musicAuthor ,
                        MPMediaItemPropertyPlaybackDuration: "\(AudioPlayer.musicDuration())",
            MPNowPlayingInfoPropertyElapsedPlaybackTime: "\(AudioPlayer.currentTime())",MPMediaItemPropertyArtwork: MPMediaItemArtwork.init(image: UIImage (data: (self.musicModel?.musicimg)!)!)] as [String : Any]
        MPNowPlayingInfoCenter.default().setValue(settings, forKey: "nowPlayingInfo")
        if AudioPlayer.progress() > 0.99 { // 自动播放下一曲 ()后台
                self.nowNum=self.nowNum+1
            if self.nowNum>=self.musicArry.count{
                self.nowNum=0
            }
            if AudioPlayer.nextsong(num: self.nowNum){
                refreashView()
            }
        }
    }

4.最后

这是刚入行的菜鸡的第一篇博客,肯定有很多不妥当的地方,希望大家见谅,项目也没做多久,对swift的理解还很浅,肯定不如大大们的法眼,我也只希望通过写这篇博客,总结一下我学到的知识,分享出来,大家互相交流学习。

附源码 https://github.com/calvinWen/SwiftMusicPlayer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容