成果

列文章目录

因为目录比拟多,每次更新这里比拟麻烦,所以举荐点击到主页,而后查看iOS Swift云音乐专栏。

目简介

这是一个应用Swift(还有OC版本)语言,从0开发一个iOS平台,靠近企业级的我的项目(我的云音乐),蕴含了根底内容,高级内容,我的项目封装,我的项目重构等常识;次要是应用零碎性能,风行的第三方框架,第三方服务,实现靠近企业级商业级我的项目。

目性能点

隐衷协定对话框
启动界面和动静解决权限
疏导界面和广告
轮播图和侧滑菜单
首页简单列表和列表排序
音乐播放和音乐列表治理
全局音乐管制条
桌面歌词和自定义款式
全局媒体控制中心
评论和回复评论
评论富文本点击
评论揭示人和话题
朋友圈动静列表和公布
高德地图定位和门路布局
阿里云OSS上传
视频播放和管制
QQ/微信登录和分享
商城/购物车\微信\支付宝领取
文本和图片聊天
音讯离线推送
主动和手动查看更新
内存透露和优化
...

发环境概述

2022年7月开发实现的,所以全部都是最新的,均匀每3年会从新制作,当初曾经是第三版了。

Xcode 13.4iOS 15

译和运行

先装置pod,用最新Xcode关上MyCloudMusic.xcworkspace,而后运行,如果要运行到真机,先登陆本人的开发者账户,如果不是付费账户,请删除推送等付费性能,更改BundleId,而后运行。

目目录构造

├── MyCloudMusic│   ├── AppDelegate.swift│   ├── Assets.xcassets #资源目录│   ├── Base.lproj│   ├── Cell #通用cell│   ├── Component #每个功能模块│   │   ├── Ad #广告相干│   │   ├── Address #播种地址相干│   ├── Config #配置目录,例如:网络地址配置│   ├── Controller #通用控制器│   ├── Extension #扩大,例如:字符串扩大│   ├── Info.plist│   ├── Manager #管理器,例如:音乐播放管理器│   ├── Model #通用模型│   ├── MyCloudMusic-Bridging-Header.h│   ├── MyCloudMusic.entitlements│   ├── Repository #数据仓库,例如:网络申请封装│   ├── Service #数据服务,例如:网络api│   ├── UI #通用UI模型│   ├── Util #工具类│   ├── Vender #通过源码形式依赖的第三方框架│   ├── View #通用View├── MyCloudMusic.xcodeproj├── MyCloudMusic.xcworkspace├── MyCloudMusicTests #测试相干├── MyCloudMusicUITests #UI测试相干├── Podfile├── Podfile.lock└── R.generated.swift #R.swfit框架生成的文件

赖框架

内容太多,只列出局部。

target 'MyCloudMusic' do  # Comment the next line if you don't want to use dynamic frameworks  use_frameworks!  # Pods for MyCloudMusic  #提供相似Android中更高层级的布局框架  #https://github.com/youngsoft/TangramKit  pod 'TangramKit'    #将资源(图片,文件等)生成类,不便到代码中办法  #例如:let icon = R.image.settingsIcon()  #let font = R.font.sanFrancisco(size: 42)  #let color = R.color.indicatorHighlight()  #let viewController = CustomViewController(nib: R.nib.customView)  #let string = R.string.localizable.welcomeWithName("Arthur Dent")  #https://github.com/mac-cain13/R.swift  pod 'R.swift'    #腾讯开源的UI框架,提供了很多性能,例如:圆角按钮,空心按钮,TextView反对placeholder  #https://github.com/QMUI/QMUIDemo_iOS  #https://qmuiteam.com/ios/get-started  pod "QMUIKit"    #图片加载  #https://github.com/SDWebImage/SDWebImage  pod 'SDWebImage'    # 网络申请框架  # https://github.com/Moya/Moya  pod 'Moya/RxSwift'  #防止每个界面定义disposeBag  #https://github.com/RxSwiftCommunity/NSObject-Rx  pod "NSObject+Rx"    #提醒框架  #https://github.com/jdg/MBProgressHUD  pod 'MBProgressHUD'    #Swift图片加载  #https://github.com/onevcat/Kingfisher  pod "Kingfisher"    #Swift扩大,像字符串,数组等  #https://github.com/SwifterSwift/SwifterSwift  pod 'SwifterSwift'    #下拉刷新  #https://github.com/CoderMJLee/MJRefresh  pod 'MJRefresh'    #富文本框架  #https://github.com/a1049145827/BSText  #OC版本:https://github.com/ibireme/YYText  pod "BSText"    #腾讯开源的偏好存储框架  #https://github.com/Tencent/MMKV  pod 'MMKV'    #腾讯WCDB是一个高效、残缺、易用的挪动数据库框架,基于SQLCipher,反对iOS, macOS和Android  #https://github.com/Tencent/wcdb  pod 'WCDB.swift'    #面向泛前端产品研发全生命周期的效率平台,查看数据库,网络申请,内存透露  #https://xingyun.xiaojukeji.com/docs/dokit/#/iosGuide    pod 'DoraemonKit/Core', :configurations => ['Debug'] #必选  #  pod 'DoraemonKit/WithGPS', '~> 3.0.4', :configurations => ['Debug'] #可选  #  pod 'DoraemonKit/WithLoad', '~> 3.0.4', :configurations => ['Debug'] #可选  #  pod 'DoraemonKit/WithLogger', '~> 3.0.4', :configurations => ['Debug'] #可选    pod 'DoraemonKit/WithDatabase',  :configurations => ['Debug'] #可选  #  pod 'DoraemonKit/WithMLeaksFinder',  :configurations => ['Debug'] #可选  #  pod 'DoraemonKit/WithWeex', '~> 3.0.4', :configurations => ['Debug'] #可选    #腾讯云开源的一款播放器组件,简略几行代码即可领有相似腾讯视频弱小的播放性能,包含横竖屏切换、清晰度抉择、手势和小窗等根底性能,还反对视频缓存,软硬解切换和倍速播放等非凡性能,相比零碎播放器,反对格局更多,兼容性更好,性能更弱小,同时还具备首屏秒开、低提早的长处,以及视频缩略图等高级能力。  #https://cloud.tencent.com/document/product/881/20208  pod 'SuperPlayer'    #图片抉择框架,预览框架  #https://github.com/longitachi/ZLPhotoBrowser  pod 'ZLPhotoBrowser'    # 阿里云OSS  # 用来上传公布带图片动静  # https://help.aliyun.com/document_detail/32055.html  pod 'AliyunOSSiOS'    #高德地图  #https://lbs.amap.com/api/ios-sdk/guide/create-project/cocoapods  #这里用的是没有IDFA的sdk,更多阐明:https://lbs.amap.com/api/ios-sdk/guide/create-project/idfa-guide  pod 'AMap3DMap-NO-IDFA'  #用户详情头部视图  # https://github.com/pujiaxin33/JXPagingView  pod 'JXPagingView/Paging'  #指示器  #https://github.com/pujiaxin33/JXSegmentedView  pod 'JXSegmentedView'    #支付宝领取  #https://docs.open.alipay.com/204/105295/  pod 'AlipaySDK-iOS'    #融云聊天  #https://doc.rongcloud.cn/im/IOS/5.X/noui/import  pod 'RongCloudIM/IMLib'    # share sdk  #https://mob.com/wiki/detailed?wiki=4&id=14  # 主模块(必须)  pod 'mob_sharesdk'  # UI模块(非必须,须要用到ShareSDK提供的分享菜单栏和分享编辑页面须要以下1行)  pod 'mob_sharesdk/ShareSDKUI'  # 平台SDK模块(对照一下平台,须要的加上。如果只须要QQ、微信、新浪微博,只须要以下3行)  pod 'mob_sharesdk/ShareSDKPlatforms/QQ'  pod 'mob_sharesdk/ShareSDKPlatforms/SinaWeibo'  #(微信sdk不带领取的命令)  #  pod 'mob_sharesdk/ShareSDKPlatforms/WeChat'  #(微信sdk带领取的命令,和下面不带领取的不能共存,只能抉择一个)  pod 'mob_sharesdk/ShareSDKPlatforms/WeChatFull'  #须要精简版QQ,微信,微博,Facebook的能够加这3个命令(精简版去掉了这4个平台的原生SDK)  #  pod 'mob_sharesdk/ShareSDKPlatforms/QQ_Lite'  #  pod 'mob_sharesdk/ShareSDKPlatforms/SinaWeibo_Lite'  #  pod 'mob_sharesdk/ShareSDKPlatforms/WeChat_Lite'  #  pod 'mob_sharesdk/ShareSDKPlatforms/Facebook_Lite'  #  pod 'mob_sharesdk/ShareSDKPlatforms/KuaiShou_Lite'  # ShareSDKPlatforms模块其余平台,按需增加  #  pod 'mob_sharesdk/ShareSDKPlatforms/TikTok'  #  pod 'mob_sharesdk/ShareSDKPlatforms/SnapChat'  #  pod 'mob_sharesdk/ShareSDKPlatforms/Oasis'  # 应用配置文件分享模块(非必须)  #  pod 'mob_sharesdk/ShareSDKConfigFile'  # 闭环分享依赖(非必须)  #  pod 'mob_sharesdk/ShareSDKRestoreScene'  # 扩大模块(在调用能够弹出咱们UI分享办法的时候是必须的)  pod 'mob_sharesdk/ShareSDKExtension'  #end share sdk  target 'MyCloudMusicTests' do    inherit! :search_paths    # Pods for testing  end  target 'MyCloudMusicUITests' do    # Pods for testing  endend

户协定对话框

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/13eaa319d44e45c9b64205f5a6b2966a~tplv-k3u1fbpfcp-watermark.image" width="300" >

应用自定义Dialog实现。

class TermServiceDialogController: BaseController, QMUIModalPresentationContentViewControllerProtocol {    var contentContainer:TGBaseLayout!    var modalController:QMUIModalPresentationViewController!    var textView:UITextView!    var disagreeButton:QMUIButton!        override func initViews() {        super.initViews()        view.layer.cornerRadius = SMALL_RADIUS        view.clipsToBounds = true        view.backgroundColor = .colorDivider        view.tg_width.equal(.fill)        view.tg_height.equal(.wrap)                //内容容器        contentContainer = TGLinearLayout(.vert)        contentContainer.tg_width.equal(.fill)        contentContainer.tg_height.equal(.wrap)        contentContainer.tg_space = 25        contentContainer.backgroundColor = .colorBackground        contentContainer.tg_padding = UIEdgeInsets(top: PADDING_OUTER, left: PADDING_OUTER, bottom: PADDING_OUTER, right: PADDING_OUTER)        contentContainer.tg_gravity = TGGravity.horz.center        view.addSubview(contentContainer)                //题目        contentContainer.addSubview(titleView)                textView = UITextView()        textView.tg_width.equal(.fill)                //超出的内容,主动反对滚动        textView.tg_height.equal(230)        textView.text="公司CFO David Wehner..."                textView.backgroundColor = .clear                //禁用编辑        textView.isEditable = false                contentContainer.addSubview(textView)                contentContainer.addSubview(primaryButton)                //不批准按钮按钮        disagreeButton=ViewFactoryUtil.linkButton()        disagreeButton.setTitle(R.string.localizable.disagree(), for: .normal)        disagreeButton.setTitleColor(.black80, for: .normal)        disagreeButton.addTarget(self, action: #selector(disagreeClick(_:)), for: .touchUpInside)        disagreeButton.sizeToFit()        contentContainer.addSubview(disagreeButton)    }        @objc func disagreeClick(_ sender:QMUIButton) {        hide()                //退出利用        exit(0)    }        func show() {        modalController = QMUIModalPresentationViewController()        modalController.animationStyle = .fade                //边距        modalController.contentViewMargins = UIEdgeInsets(top: PADDING_LARGE2, left: PADDING_LARGE2, bottom: PADDING_LARGE2, right: PADDING_LARGE2)                //点击内部不暗藏        modalController.isModal = true                //设置要显示的内容控件        modalController.contentViewController = self                modalController.showWith(animated: true)    }        lazy var titleView: UILabel = {        let r = UILabel()        r.tg_width.equal(.fill)        r.tg_height.equal(.wrap)        r.text = "题目"        r.textColor = .colorOnSurface        r.font = UIFont.boldSystemFont(ofSize: TEXT_LARGE2)        r.textAlignment = .center        return r    }()        lazy var primaryButton: QMUIButton = {        let r = ViewFactoryUtil.primaryHalfFilletButton()        r.setTitle(R.string.localizable.agree(), for: .normal)        return r    }()}````## 导界面![在这里插入图片形容](https://img-blog.csdnimg.cn/3e22061d7ea84ba695eeffd06024cd3f.png)疏导界面比较简单,就是多个图片能够左右滚动。

class GuideController: BaseLogicController {

var bannerView:YJBannerView!override func initViews() {    super.initViews()    initLinearLayoutSafeArea()        container.tg_space = PADDING_OUTER        bannerView = YJBannerView()    bannerView.backgroundColor = .clear    bannerView.dataSource = self    bannerView.delegate = self    bannerView.tg_width.equal(.fill)    bannerView.tg_height.equal(.fill)        //设置如果找不到图片显示的图片    bannerView.emptyImage = R.image.placeholderError()        //设置占位图    bannerView.placeholderImage = R.image.placeholder()        //设置轮播图外部显示图片的时候调用什么办法    bannerView.bannerViewSelectorString = "sd_setImageWithURL:placeholderImage:"        //设置指示器默认色彩    bannerView.pageControlNormalColor = .black80        //高亮的色彩    bannerView.pageControlHighlightColor = .colorPrimary        //从新加载数据    bannerView.reloadData()        container.addSubview(bannerView)        //按钮容器    let controlContainer = TGLinearLayout(.horz)    controlContainer.tg_bottom.equal(PADDING_OUTER)    controlContainer.tg_width ~= .fill    controlContainer.tg_height.equal(.wrap)        //程度拉升,左,中,右间距一样    controlContainer.tg_gravity = TGGravity.horz.among    container.addSubview(controlContainer)        //登录注册按钮    let primaryButton = ViewFactoryUtil.primaryButton()    primaryButton.setTitle(R.string.localizable.loginOrRegister(), for: .normal)    primaryButton.addTarget(self, action: #selector(primaryClick(_:)), for: .touchUpInside)    primaryButton.tg_width.equal(BUTTON_WIDTH_MEDDLE)    controlContainer.addSubview(primaryButton)        //立刻体验按钮    let enterButton = ViewFactoryUtil.primaryOutlineButton()    enterButton.setTitle(R.string.localizable.experienceNow(), for: .normal)    enterButton.addTarget(self, action: #selector(enterClick(_:)), for: .touchUpInside)    enterButton.tg_width.equal(BUTTON_WIDTH_MEDDLE)    controlContainer.addSubview(enterButton)    }///登录注册按钮点击/// - Parameter sender: <#sender description#>@objc func primaryClick(_ sender:QMUIButton) {    AppDelegate.shared.toLogin()}///立刻体验按钮点击/// - Parameter sender: <#sender description#>@objc func enterClick(_ sender:QMUIButton) {    AppDelegate.shared.toMain()}

}

// MARK: - YJBannerViewDataSource
extension GuideController:YJBannerViewDataSource{

/// banner数据源////// - Parameter bannerView: <#bannerView description#>/// - Returns: <#return value description#>func bannerViewImages(_ bannerView: YJBannerView!) -> [Any]! {    return ["guide1","guide2","guide3","guide4","guide5"]}/// 自定义Cell/// 复写该办法的目标是/// 设置图片的缩放模式////// - Parameters:///   - bannerView: <#bannerView description#>///   - customCell: <#customCell description#>///   - index: <#index description#>/// - Returns: <#return value description#>func bannerView(_ bannerView: YJBannerView!, customCell: UICollectionViewCell!, index: Int) -> UICollectionViewCell! {    //将cell类型转为YJBannerViewCell    let cell = customCell as! YJBannerViewCell    //设置图片的缩放模式为    //从核心填充    //多余的裁剪掉    cell.showImageViewContentMode = .scaleAspectFit    return cell}

}

// MARK: - YJBannerViewDelegate
extension GuideController:YJBannerViewDelegate{

}

## 广告界面![在这里插入图片形容](https://img-blog.csdnimg.cn/d2bc980948f74aaca024e09f9c7924cc.png)实现图片广告和视频广告,广告数据是在首页是缓存到本地,目标是在启动界面加载更快,因为实在我的项目中,大部分我的项目启动页面广告工夫一共就5秒,如果太长了用户体验不好,如果是从网络申请,那么网络可能就耗时2秒左右,所以导致就美哟多少工夫显示广告了。### 广告

func downloadAd(_ data:Ad,_ path:URL) {

let destination: DownloadRequest.Destination = { _, _ in    return (path, [.removePreviousFile, .createIntermediateDirectories])}AF.download(data.icon.absoluteUri(), to: destination).response { response in    if response.error == nil, let filePath = response.fileURL?.path {        print("ad downloaded success \(filePath)")    }}

}

### 广告

func showVideoAd(_ data:URL) {

//播放利用内嵌入视频,放根目录中//同样其余的文件,也能够通过这种形式读取//var data=Bundle.main.url(forResource: "ixueaeduTestVideo", withExtension: ".mp4")!player = AVPlayer(url: data)//静音player!.isMuted = true/// 增加进度监听player!.addPeriodicTimeObserver(forInterval: CMTime(value: CMTimeValue(1.0), timescale: 60), queue: DispatchQueue.main, using: {time in    if self.player == nil {        return    }        //播放工夫    let current = Float(CMTimeGetSeconds(time))        //总工夫    let duration = Float(CMTimeGetSeconds(self.player!.currentItem!.duration))        if current==duration {        //视频播放完结        self.next()    } else {        self.skipView.setTitle(R.string.localizable.skipAdCount(Int(duration-current)), for: .normal)        self.skipView.tg_width.equal(.wrap)        self.skipView.setNeedsLayout()    }})//显示图像playerLayer = AVPlayerLayer(player: player)//从核心等比缩放,齐全显示控件playerLayer?.videoGravity = .resizeAspectFillview.layer.insertSublayer(playerLayer!, at: 0)

}

显示图片就是显示本地图片了,没什么难点,就不贴代码了。## 首页/歌单详情/黑胶唱片界面![在这里插入图片形容](https://img-blog.csdnimg.cn/4f0846d0571d45fca4a8d7d84841f939.png)首页没有顶部是轮播图,而后是能够左右的菜单,接下来是热门歌单,举荐单曲,最初是首页排序模块;整体上应用RecycerView实现,轮播图:

//取出一个Cell
let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! BannerCell

//绑定数据
cell.bind(data as! BannerData)

cell.bannerClick = {[weak self] data in

self?.processAdClick(data)

}

举荐歌单

/// 协定
protocol SheetGroupDelegate:NSObjectProtocol {

/// 歌单点击回调/// - Parameter data: 点击的歌单对象func sheetClick(data:Sheet)

}

class SheetGroupCell: BaseTableViewCell {

static let NAME = "SheetGroupCell"var datum:Array<Sheet> = []var cellWidth:CGFloat!var cellHeight:CGFloat!var spanCount:CGFloat = 3weak open var delegate: SheetGroupDelegate?override func initViews() {    super.initViews()    //分割线    container.addSubview(ViewFactoryUtil.smallDivider())        //题目    container.addSubview(titleView)        container.addSubview(collectionView)        collectionView.register(SheetCell.self, forCellWithReuseIdentifier: Constant.CELL)}override func getContainerOrientation() -> TGOrientation {    return .vert}func bind(_ data:SheetData) {    //计算每个cell宽度        //屏幕宽度-外边距16*2-(self.spanCount-1)*5    cellWidth = (SCREEN_WIDTH-PADDING_OUTER*CGFloat(2) - (spanCount - CGFloat(1))*PADDING_SMALL)/spanCount        //cell高度,5:图片和题目边距,40:2行文字高度    cellHeight = cellWidth + PADDING_SMALL + 40        //计算能够显示几行    let rows = ceil(CGFloat(data.datum.count) / spanCount)        //CollectionView高度等于,行数*行高,10:垂直方向每个cell间距    let viewHeight = rows * (cellHeight + PADDING_MEDDLE)        collectionView.tg_height.equal(viewHeight)        datum.removeAll()        datum += data.datum    collectionView.reloadData()}/// 题目控件lazy var titleView: ItemTitleView = {    let r = ItemTitleView()    r.titleView.text = R.string.localizable.recommendSheet()    return r}()lazy var collectionView: UICollectionView = {    let r = ViewFactoryUtil.collectionView()    r.delegate = self    r.dataSource = self    r.isScrollEnabled = false        return r}()

}

/// CollectionView数据源和代理
extension SheetGroupCell:UICollectionViewDataSource,UICollectionViewDelegate {

/// 有多少个/// - Parameters:///   - collectionView: <#collectionView description#>///   - section: <#section description#>/// - Returns: <#description#>func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {    return datum.count}/// 返回cell/// - Parameters:///   - collectionView: <#collectionView description#>///   - indexPath: <#indexPath description#>/// - Returns: <#description#>func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {    let data = datum[indexPath.row]        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constant.CELL, for: indexPath) as! SheetCell        cell.bind(data)        return cell}/// item点击/// - Parameters:///   - collectionView: <#collectionView description#>///   - indexPath: <#indexPath description#>func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {    if let d = delegate {        d.sheetClick(data:datum[indexPath.row])    }}

}

/// UICollectionViewDelegateFlowLayout
extension SheetGroupCell:UICollectionViewDelegateFlowLayout{

/// 返回CollectionView外面的Cell到CollectionView的间距/// - Parameters:///   - collectionView: <#collectionView description#>///   - collectionViewLayout: <#collectionViewLayout description#>///   - section: <#section description#>/// - Returns: <#description#>func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {    return UIEdgeInsets(top: 0, left: PADDING_OUTER, bottom: PADDING_OUTER, right: PADDING_OUTER)}/// 返回每个Cell的行间距/// - Parameters:///   - collectionView: <#collectionView description#>///   - collectionViewLayout: <#collectionViewLayout description#>///   - section: <#section description#>func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {    return PADDING_MEDDLE}/// 返回每个Cell的列间距/// - Parameters:///   - collectionView: <#collectionView description#>///   - collectionViewLayout: <#collectionViewLayout description#>///   - section: <#section description#>/// - Returns: <#description#>func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {    return PADDING_SMALL}/// cell尺寸/// - Parameters:///   - collectionView: <#collectionView description#>///   - collectionViewLayout: <#collectionViewLayout description#>///   - indexPath: <#indexPath description#>/// - Returns: <#description#>func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {    return CGSize(width: cellWidth, height: cellHeight)}

}

### 详情顶部是歌单信息,通过Cell实现,底部是列表,显示歌单内容的音乐,点击音乐进入黑胶唱片播放界面。

class SheetDetailController: BaseMusicPlayerController {

/// 数据idvar id:String!var data:Sheet!//背景var backgroundImageView: UIImageView!//背景含糊var backgroundVisual: UIVisualEffectView!override func initViews() {    super.initViews()        //增加背景图片控件    backgroundImageView = UIImageView()    backgroundImageView.clipsToBounds = true    backgroundImageView.alpha = 0    backgroundImageView.contentMode = .scaleAspectFill    view.addSubview(backgroundImageView)        //背景含糊成果    let blur = UIBlurEffect(style: .dark)    backgroundVisual = UIVisualEffectView(effect: blur)    backgroundImageView.addSubview(backgroundVisual)        //初始化TableView构造    initTableViewSafeArea()        //设置状态栏为亮色(文字是红色)    setStatusBarLight()        setToolbarLight()        title = R.string.localizable.sheet()        //注册单曲    tableView.register(SongCell.self, forCellReuseIdentifier: Constant.CELL)    tableView.register(SheetInfoCell.self, forCellReuseIdentifier: SheetInfoCell.NAME)        //注册section    tableView.register(SongGroupHeaderView.self, forHeaderFooterViewReuseIdentifier: SongGroupHeaderView.NAME)    tableView.bounces = false}override func initDatum() {    super.initDatum()    loadData()}func loadData() {    DefaultRepository.shared        .sheetDetail(id)        .subscribeSuccess {[weak self] data in            self?.show(data.data!)        }.disposed(by: rx.disposeBag)}func show(_ data:Sheet) {    self.data=data        backgroundImageView.show(data.icon)        //应用动画显示背景图片    UIView.animate(withDuration: 0.3) {        //透明度设置为1        self.backgroundImageView.alpha = 1    }        //第一组    var groupData=SongGroupData()    groupData.datum = [data]    datum.append(groupData)        //第二组    if let r = data.songs {        if !r.isEmpty {            //有音乐才设置            //设置数据            groupData=SongGroupData()            groupData.datum = r            datum.append(groupData)            superFooterContainer.backgroundColor = .colorLightWhite        }    }    tableView.reloadData()}/// 获取列表类型////// - Parameter data: <#data description#>/// - Returns: <#return value description#>func typeForItemAtData(_ data:Any) -> MyStyle {    if data is Sheet {        return .sheet    }        return .song}/// 播放音乐/// - Parameter data: <#data description#>func play(_ data:Song) {    //把以后歌单所有音乐设置到播放列表    //有些利用    //可能会实现增加到曾经播放列表性能    MusicListManager.shared().setDatum(self.data.songs!)        //播放以后音乐    MusicListManager.shared().play(data)        startMusicPlayerController()}override func viewDidLayoutSubviews() {    super.viewDidLayoutSubviews()    backgroundImageView.frame = view.bounds    backgroundVisual.frame = backgroundImageView.bounds}@objc func commentClick() {    CommentController.start(navigationController!)}

}

extension SheetDetailController{

/// 有多少组/// - Parameter tableView: <#tableView description#>/// - Returns: <#description#>func numberOfSections(in tableView: UITableView) -> Int {    return datum.count}/// 以后组有多少个/// - Parameters:///   - tableView: <#tableView description#>///   - section: <#section description#>/// - Returns: <#description#>override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {    let data = datum[section] as! SongGroupData    return data.datum.count}/// 返回section view/// - Parameters:///   - tableView: <#tableView description#>///   - section: <#section description#>/// - Returns: <#description#>func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {    //取出组数据    let groupData=datum[section] as! SongGroupData        //获取header    let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: SongGroupHeaderView.NAME) as! SongGroupHeaderView        header.bind(groupData)        header.playAllClick = {[weak self] in        let groupData = self?.datum[1] as! SongGroupData        self?.play(groupData.datum[0] as! Song)    }        return header}/// 返回以后地位的cell/// - Parameters:///   - tableView: <#tableView description#>///   - indexPath: <#indexPath description#>/// - Returns: <#description#>override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    let groupData = datum[indexPath.section] as! SongGroupData    let data = groupData.datum[indexPath.row]        let type = typeForItemAtData(data)        switch type {    case .sheet:        let cell = tableView.dequeueReusableCell(withIdentifier: SheetInfoCell.NAME, for: indexPath) as! SheetInfoCell        cell.bind(data as! Sheet)                cell.commentCountView.addTarget(self, action: #selector(commentClick), for: .touchUpInside)                return cell    default:        let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! SongCell        cell.bind(data as! Song)        cell.indexView.text = "\(indexPath.row + 1)"                return cell    }        }/// header高度/// - Parameters:///   - tableView: <#tableView description#>///   - section: <#section description#>/// - Returns: <#description#>func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {    if section == 1 {        return 50    }        //其余组不显示section    return 0}/// cell点击/// - Parameters:///   - tableView: <#tableView description#>///   - indexPath: <#indexPath description#>func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {    let groupData = datum[indexPath.section] as! SongGroupData    let data = groupData.datum[indexPath.row]        let type = typeForItemAtData(data)        if type == .song {        play(data as! Song)    }}

}

extension SheetDetailController{

/// 启动办法/// - Parameters:///   - controller: <#controller description#>///   - id: <#id description#>static func start(_ controller:UINavigationController,_ id:String) {    let target = SheetDetailController()    target.id=id    controller.pushViewController(target, animated: true)}

}

### 唱片下面是黑胶唱片,和网易云音乐差不多,随着音乐滚动或暂停,顶部是管制相干,音乐播放逻辑是封装到MusicPlayerManager中:

class MusicPlayerManager : NSObject{

/// 保留音乐播放进度的距离private static let SAVE_PROGRESS_TIME_INTERVAL:TimeInterval = 2private static var instance:MusicPlayerManager?/// 以后播放的音乐var data:Song?/// 播放器private var player:AVPlayer!/// 播放状态var status:PlayStatus = .none/// 定时器返回的对象private var playTimeObserve:Any?///播放结束回调var complete:((_ data:Song)->Void)!private var lastSaveProgressTime:TimeInterval = 0/// 代理对象,目标是将不同的状态散发进来weak open var delegate:MusicPlayerManagerDelegate?{    didSet{        if let _ = self.delegate {            //有代理                        //判断是否有音乐在播放            if self.isPlaying() {                //有音乐在播放                                //启动定时器                startPublishProgress()            }        }else {            //没有代理                        //进行定时器            stopPublishProgress()        }    }}/// 获取单例的播放管理器////// - Returns: <#return value description#>static func shared() -> MusicPlayerManager {    if instance == nil {        instance = MusicPlayerManager()    }        return instance!}private override init() {    super.init()    player = AVPlayer()}/// 播放/// - Parameters:///   - uri: 相对音乐地址///   - data: 音乐对象func play(uri:String,data:Song) {    //申请获取音频会话焦点    SuperAudioSessionManager.requestAudioFocus()        //保留音乐对象    self.data = data        status = .playing        var url:URL?=nil    if uri.starts(with: "http") {        //网络地址        url = URL(string: uri)    } else {        //本地地址        url = URL(fileURLWithPath: uri)    }        //创立一个播放Item    let item = AVPlayerItem(url: url!)        //替换掉原来的播放Item    player.replaceCurrentItem(with: item)        //播放    player.play()        //回调代理    if let r = delegate {        r.onPlaying(data: data)    }        //设置监听器    //因为监听器是针对PlayerItem的    //所以说播放了音乐在这里设置    initListeners()        //启动进度散发定时器    startPublishProgress()        prepareLyric()}/// 暂停func pause() {    //更改状态    status = .pause        //暂停    player.pause()        //回调代理    if let r = delegate {        r.onPaused(data: data!)    }        //移除监听器    removeListeners()        //进行进度散发定时器    stopPublishProgress()}/// 持续播放func resume() {    //申请获取音频会话焦点    SuperAudioSessionManager.requestAudioFocus()        status = .playing        player.play()        //回调代理    if let r = delegate {        r.onPlaying(data: data!)    }        //设置监听器    initListeners()        //启动进度散发定时器    startPublishProgress()}/// 是否在播放/// - Returns: <#description#>func isPlaying() -> Bool {    return status == .playing}/// 挪动到指定地位播放func seekTo(data:Float) {    let positionTime = CMTime(seconds: Double(data), preferredTimescale: 1)    player.seek(to: positionTime)}...private func stopPublishProgress() {    if let playTimeObserve = playTimeObserve {        player.removeTimeObserver(playTimeObserve)        self.playTimeObserve = nil    }}private func initListeners() {    //KVO形式监听播放状态    //KVC:Key-Value Coding,另一种获取对象字段的值,相似字典    //KVO:Key-Value Observing,建设在KVC根底上,可能察看一个字段值的扭转    player.currentItem?.addObserver(self, forKeyPath: MusicPlayerManager.STATUS, options: .new, context: nil)        //监听音乐缓冲状态    player.currentItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)        //播放完结事件    NotificationCenter.default.addObserver(self, selector: #selector(onComplete(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)}/// 移除监听器private func removeListeners() {    player.currentItem?.removeObserver(self, forKeyPath: MusicPlayerManager.STATUS)    player.currentItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")}/// 播放结束了回调@objc func onComplete(_ sender:Notification) {    complete(data!)}/// KVO监听回调办法////// - Parameters:///   - keyPath: <#keyPath description#>///   - object: <#object description#>///   - change: <#change description#>///   - context: <#context description#>override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {    //判断监听的字段    if MusicPlayerManager.STATUS == keyPath {        //播放状态        switch player.status {        case .readyToPlay:            //筹备播放实现了                        //音乐的总工夫            self.data!.duration = Float(CMTimeGetSeconds(player.currentItem!.asset.duration))                        //回调代理            delegate?.onPrepared(data:data!)                        updateMediaInfo()        case .failed:            //播放失败了            status = .error                        delegate?.onError(data: data!)        default:            //未知状态            status = .none        }    }        }/// 更新零碎媒体控制中心信息/// 不须要更新进度到控制中心/// 他那边会主动倒计时/// 这部分能够重构到公共类,因为像播放视频也能够更新到零碎媒体核心private func updateMediaInfo() {    //下载图片    //这部分能够封装    //因为其余界面可能也会用    let manager = SDWebImageManager.shared    if data?.icon == nil {        self.setMediaInfo(R.image.placeholder()!)    } else {        let url = URL(string: data!.icon!.absoluteUri())        //下载图片        manager.loadImage(with: url, options: .progressiveLoad) { receivedSize, expectedSize, targetURL in        } completed: { image, data, error, cacheType, finished, imageURL in            print("load song image success \(url)")            if let r = image {                self.setMediaInfo(r)            }        }    }}func prepareLyric() {    //歌词解决    //实在我的项目可能会    //将歌词这个局部拆分到其余组件中    if data!.parsedLyric != nil && data!.parsedLyric!.datum.count > 0 {        //解析好了        onLyricReady()    } else if SuperStringUtil.isNotBlank(data!.lyric){        //有歌词,然而没有解析        parseLyric()    } else {        //没有歌词,并且不是本地音乐才申请        //实在我的项目中能够会缓存歌词        //获取歌词数据        DefaultRepository.shared            .songDetail(data!.id)            .subscribeSuccess { data in                //申请胜利                self.data!.style = data.data!.style                self.data!.lyric = data.data!.lyric                                self.parseLyric()            }    }}func parseLyric() {    if SuperStringUtil.isNotBlank(data?.lyric) {        //有歌词                //在这里解析的益处是        //里面不必管,间接应用        data?.parsedLyric = LyricParser.parse(data!.style,data!.lyric!)    }        //告诉歌词筹备好了    onLyricReady()}func onLyricReady() {    if let r = delegate {        r.onLyricReady(data: data!)    }}static let STATUS = "status"

}

/// 播放状态枚举
enum PlayStatus {

case none //未知case pause //暂停了case playing //播放中case prepared //筹备中case completion //以后这一首音乐播放实现case error

}

/// 播放管理器代理
protocol MusicPlayerManagerDelegate:NSObjectProtocol{

/// 播放器筹备结束了/// 能够获取到音乐总时长func onPrepared(data:Song)/// 暂停了func onPaused(data:Song)/// 正在播放func onPlaying(data:Song)/// 进度回调func onProgress(data:Song)/// 歌词数据筹备好了func onLyricReady(data:Song)/// 出错了func onError(data:Song)

}

音乐列表逻辑封装到MusicListManager:

class MusicListManager {

private static var instance:MusicListManager?/// 以后音乐对象var data:Song?//播放列表var datum:[Song] = []/// 播放管理器var musicPlayerManager:MusicPlayerManager!/// 是否播放了var isPlay = false/// 循环模式,默认列表循环var model:MusicPlayRepeatModel = .list/// 获取单例的播放列表管理器////// - Returns: <#return value description#>static func shared() -> MusicListManager {    if instance == nil {        instance = MusicListManager()    }        return instance!}private init() {    //初始化音乐播放管理器    musicPlayerManager = MusicPlayerManager.shared()        //设置播放结束回调    musicPlayerManager.complete = {d in        //判断播放循环模式        if self.model == .one {            //单曲循环            self.play(d)        }else{            //其余模式            self.play(self.next())        }    }        initPlayList()}func initPlayList() {    datum.removeAll()        //查问播放列表    let datum=SuperDatabaseManager.shared.findPlayList()    if datum.count > 0 {        //增加到当初的播放列表        self.datum += datum                //获取最初播放音乐id        let id = PreferenceUtil.getLastPlaySongId()        if SuperStringUtil.isNotBlank(id) {            //有最初播放音乐的id            //在播放列表中找到该音乐            for it in datum {                if it.id == id {                    data = it                }            }                        if data == nil {                //示意没找到                //可能各种起因                defaultPlaySong()            } else {                //找到了            }        }else{            //如果没有最初播放音乐            //默认就是第一首            defaultPlaySong()        }                musicPlayerManager.data = data        musicPlayerManager.prepareLyric()    }        

// sendMusicListChanged()

}func defaultPlaySong() {    data = datum[0]}/// 设置音乐列表/// - Parameter datum: <#datum description#>func setDatum(_ datum:[Song]) {    //将原来数据list标记设置为false   DataUtil.changePlayListFlag(self.datum, false)   //保留到数据库   saveAll()        //清空原来的数据    self.datum.removeAll()        //增加新的数据    self.datum += datum        //更改播放列表标记    DataUtil.changePlayListFlag(self.datum, true)    //保留到数据库    saveAll()    sendMusicListChanged()}/// 播放/// - Parameter data: <#data description#>func play(_ data:Song) {    self.data = data        //标记为播放了    isPlay = true        var path:String!        //查问是否有下载工作    let downloadInfo = AppDelegate.shared.getDownloadManager().findDownloadInfo(data.id)    if downloadInfo != nil && downloadInfo.status == .completed {        //下载实现了       //播放本地音乐        path = StorageUtil.documentUrl().appendingPathComponent(downloadInfo.path).path        print("MusicListManager play offline \(path!) \(data.uri!)")    } else {        //播放在线音乐        path = data.uri.absoluteUri()        print("MusicListManager play online \(path!) \(data.uri!)")    }        musicPlayerManager.play(uri: path, data: data)        //设置最初播放音乐的Id    PreferenceUtil.setLastPlaySongId(data.id)}/// 暂停func pause() {    musicPlayerManager.pause()}/// 持续播放func resume() {    if isPlay {        //原来曾经播放过        //也就说播放器曾经初始化了        musicPlayerManager.resume()    } else {        //到这里,是利用开启后,第一次点持续播放        //而这时外部其实还没有筹备播放,所以应该调用播放        play(data!)                //判断是否须要持续播放        if data!.progress>0 {            //有播放进度            //就从上一次地位开始播放            musicPlayerManager.seekTo(data: data!.progress)        }    }}@discardableResult/// 更改循环模式func changeLoopModel() -> MusicPlayRepeatModel {    //将以后循环模式转为int    var model = self.model.rawValue        //循环模式+1    model += 1        //判断边界    if model > MusicPlayRepeatModel.random.rawValue {        //超出了范畴        model = 0    }        self.model = MusicPlayRepeatModel(rawValue: model)!        return self.model}/// 获取上一个func previous() -> Song {    var index = 0    switch model {    case .random:        //随机循环                //在0~datum.size-1范畴中        //产生一个随机数        index = Int(arc4random()) % datum.count    default:        //列表循环        let datumOC = datum as NSArray        index = datumOC.index(of: data!)                //如果以后播放的音乐是最初一首音乐        if index == 0 {            //以后播放的是第一首音乐            index = datum.count - 1        } else {            index -= 1        }    }        return datum[index]}...

}

//音乐循环状态
enum MusicPlayRepeatModel:Int {

case list=0 //列表循环case one //单曲循环case random //列表随机

}

外界对立应用播放列表管理器播放音乐,上一曲下一曲:

@objc func previousClick(_ sender:QMUIButton) {

MusicListManager.shared().play(MusicListManager.shared().previous())

}

@objc func playClick(_ sender:QMUIButton) {

playOrPause()

}

@objc func nextClick(_ sender:QMUIButton) {

MusicListManager.shared().play(MusicListManager.shared().next())

}

## 歌词<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f7cb8ed4d71459aabd5f2600e84ece8~tplv-k3u1fbpfcp-watermark.image" width="300" >歌词实现了LRC,KSC两种歌词,封装到LyricListView,单个歌词行封装到LyricView中,外界间接应用LyricListView就行:

/// 显示歌词数据
func showLyricData() {

lyricView.setData(MusicListManager.shared().data!.parsedLyric)

}

歌词控件封装:

class LyricListView: BaseRelativeLayout {

var data:Lyric?var tableView:UITableView!var datum:[Any] = []/// 以后工夫歌词行数var lyricLineNumber:Int = 0/// 歌词填充多个占位数据var lyricPlaceholderSize = 0/// 是否曾经调用了reloadDatavar isReloadData:Bool = false/// 歌词拖拽成果容器var lyricDragContainer:TGLinearLayout!/// 拖拽地位歌词工夫var timeView:UILabel!/// 是否在拖拽状态var isDrag:Bool = false/// 滚动时,以后这行歌词var scrollSelectedLyricLine:LyricLine?override func initViews() {    super.initViews()    //设置束缚    tg_width.equal(.fill)    tg_height.equal(.fill)        //tableView    tableView = ViewFactoryUtil.tableView()    tableView.delegate = self    tableView.dataSource = self    addSubview(tableView)        //注册歌词cell    tableView.register(LyricCell.self, forCellReuseIdentifier: Constant.CELL)        //创立一个程度方向容器    lyricDragContainer = TGLinearLayout(.horz)    lyricDragContainer.hide()    lyricDragContainer.tg_horzMargin(PADDING_OUTER)    lyricDragContainer.tg_width.equal(.fill)    lyricDragContainer.tg_height.equal(.wrap)    //控件之间间距    lyricDragContainer.tg_space = PADDING_MEDDLE    //内容垂直居中    lyricDragContainer.tg_gravity = TGGravity.vert.center    //居中    lyricDragContainer.tg_centerY.equal(0)    addSubview(lyricDragContainer)        //播放按钮    let playView = QMUIButton()    playView.tg_width.equal(15)    playView.tg_height.equal(15)    playView.setImage(R.image.play()!.withTintColor(), for: .normal)    playView.tintColor = .colorLightWhite    //图片齐全显示到控件外面    playView.contentMode = .scaleAspectFit    playView.addTarget(self, action: #selector(playClick(_:)), for: .touchUpInside)    lyricDragContainer.addSubview(playView)        //分割线    let dividerView = ViewFactoryUtil.smallDivider()    dividerView.backgroundColor = .colorLightWhite    lyricDragContainer.addSubview(dividerView)        //工夫    timeView = UILabel()    timeView.tg_width.equal(.wrap)    timeView.tg_height.equal(.wrap)    timeView.text = "00:00"    timeView.textColor = .colorLightWhite    lyricDragContainer.addSubview(timeView)}/// 这个办法会调用屡次计算,最初一次才是最精确的值override func layoutSubviews() {    super.layoutSubviews()    if lyricPlaceholderSize > 0 {        return    }        lyricPlaceholderSize = Int(ceil( Double(tableView.frame.height)/2.0/44.0))}func setData(_ data:Lyric?) {    self.data=data        if lyricPlaceholderSize>0 {       //曾经计算了填充数量       next()   }}func next() {    //清空原来的歌词    datum.removeAll()        if let r = data {        //增加占位数据        addLyricFillData()                datum += r.datum                //增加占位数据        addLyricFillData()    }    isReloadData=true    tableView.reloadData()}//显示拖拽成果func showDragView() {    if isLyricEmpty() {        //没有歌词不能拖拽        return    }        isDrag=true    lyricDragContainer.show()}func prepareScrollLyricView() {    //勾销原来的工作    NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideDragView), object: nil)    //4秒后暗藏拖拽控件    perform(#selector(hideDragView), with: nil, afterDelay: 4.0)}@objc func hideDragView() {    isDrag=false        //勾销原来的工作    NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideDragView), object: nil)        lyricDragContainer.hide()}@objc func playClick(_ sender:QMUIButton) {    if let r = scrollSelectedLyricLine {        //回调回来是毫秒,要转为秒        MusicListManager.shared().seekTo(Float(r.startTime/1000))        //马上显示歌词滚动        hideDragView()    }}...

}

extension LyricListView:QMUITableViewDelegate,QMUITableViewDataSource{

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {    return datum.count}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    let data = datum[indexPath.row]        let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! LyricCell    cell.bind(data, self.data!.isAccurate)        return cell}/// 开始拖拽/// - Parameter scrollView: <#scrollView description#>func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {    showDragView()}/// 拖拽完结/// - Parameters:///   - scrollView: <#scrollView description#>///   - decelerate: <#decelerate description#>func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {    if !decelerate {        //如果不须要加速,就延时后,显示歌词        prepareScrollLyricView()    }}/// 惯性拖拽完结/// - Parameter scrollView: <#scrollView description#>func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {    prepareScrollLyricView()}/// 滑动中/// - Parameter scrollView: <#scrollView description#>func scrollViewDidScroll(_ scrollView: UIScrollView) {    if isDrag {        //只有手动拖拽的时候才解决                let offsetY  = scrollView.contentOffset.y                //依据滚动间隔计算出index        let index = Int((offsetY+tableView.frame.height/2)/44)                //获取歌词对象        var lyric:Any!        if (index < 0) {            //如果计算出的index小于0            //就默认第一个歌词对象            lyric = datum.first        }else if (index > datum.count - 1) {            //大于最初一个歌词对象(蕴含填充数据)            //就是最初一行数据            lyric = datum.last        }else {            //如果在列表范畴内            //就间接去对应地位的数据            lyric = datum[index]        }                //设置滚动工夫        //判断是否是填充数据        if lyric is String {            //填充数据            timeView.text = ""        } else {            //实在歌词数据            //保留到一个字段上            scrollSelectedLyricLine = lyric as! LyricLine                        //将开始工夫转为秒            let startTime = Float( scrollSelectedLyricLine!.startTime / 1000)                        timeView.text = SuperDateUtil.second2MinuteSecond(startTime)        }            }}

}

### 控制器应用了能够通过零碎媒体控制器,告诉栏,锁屏界面,耳机,蓝牙耳机等设施管制媒体播放暂停,只须要把媒体信息更新到零碎:

private func setMediaInfo(_ image:UIImage) {

//初始化一个可变字典var songInfo:[String:Any] = [:]//封面let albumArt = MPMediaItemArtwork(boundsSize: CGSize(width: 100, height: 100)) { size -> UIImage in    return image}//封面songInfo[MPMediaItemPropertyArtwork]=albumArt//歌曲名称songInfo[MPMediaItemPropertyTitle]=data!.title//歌手songInfo[MPMediaItemPropertyArtist]=data!.singer.nickname//专辑名称//因为服务端没有返回专辑的数据//所以这里就写死数据就行了songInfo[MPMediaItemPropertyAlbumTitle]="这是专辑名称"//流派//songInfo[MPMediaItemPropertyGenre]="这是流派"//总时长songInfo[MPMediaItemPropertyPlaybackDuration]=data!.duration//曾经播放的时长songInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime]=data!.progress//歌词songInfo[MPMediaItemPropertyLyrics]="这是歌词"//设置到零碎MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo

}

### 媒体管制

/// 接管近程管制事件
/// 能够接管到媒体控制中心的事件
///
/// - Parameter event: <#event description#>
override func remoteControlReceived(with event: UIEvent?) {

print("AppDelegate remoteControlReceived:\(event?.type),\(event?.subtype)")//判断是不是近程管制事件if event?.type == UIEvent.EventType.remoteControl {    //是近程管制事件    //是否有音乐    if MusicListManager.shared().data == nil {        //以后播放列表中没有音乐        return    }    //判断事件类型    switch event!.subtype {    case .remoteControlPlay:        //点击了播放按钮        print("AppDelegate play")        MusicListManager.shared().resume()    case .remoteControlPause:        //点击了暂停        print("AppDelegate pause")        MusicListManager.shared().pause()    case .remoteControlNextTrack:        //下一首        //双击iPhone有线耳机上的管制按钮        print("AppDelegate next")        let song = MusicListManager.shared().next()        MusicListManager.shared().play(song)    case .remoteControlPreviousTrack:        //上一首        //三击iPhone有线耳机上的管制按钮        print("AppDelegate previouse")        let song = MusicListManager.shared().previous()        MusicListManager.shared().play(song)    case .remoteControlTogglePlayPause:        //单击iPhone有线耳机上的管制按钮        print("AppDelegate toggle play pause")        //播放或者暂停        if MusicPlayerManager.shared().isPlaying() {            MusicListManager.shared().pause()        } else {            MusicListManager.shared().resume()        }    default:        break    }}

}

## 登录/注册/验证码登录![在这里插入图片形容](https://img-blog.csdnimg.cn/8f97044067314f7aa1c13e65b3bc7ed2.png)登录注册没有多大难度,用户名和明码登录,就是把信息传递到服务端,能够加密后在传输,服务端判断登录胜利,返回一个标记,客户端保留,其余须要的登录的接口带上;验证码登录就是用验证码代替明码,发送验证码都是服务端发送,客户端只须要调用接口。## 评论![在这里插入图片形容](https://img-blog.csdnimg.cn/13e04cfb79994224aa5a3d1fb3a11d9b.png)评论列表包含下拉刷新,上拉加载更多,点赞,公布评论,回复评论,Emoji,话题和揭示人点击,抉择好友,抉择话题等。### 刷新和下拉加载更多外围逻辑就只须要更改page就行了

//下拉刷新
let header=MJRefreshNormalHeader {

[weak self] inself?.loadData()

}

//暗藏题目
header.stateLabel?.isHidden = true

// 暗藏工夫
header.lastUpdatedTimeLabel?.isHidden = true
tableView.mj_header=header

//上拉加载更多
let footer = MJRefreshAutoNormalFooter {

[weak self] inself?.loadMore()

}

// 设置闲暇时文字
footer.setTitle("", for: .idle)

tableView.mj_footer = footer

### 人和话题点击通过正则表达式,找到非凡文本,而后应用富文本实现点击。

/// 解决文本点击事件
func processContent(_ data:String) -> NSAttributedString {

return RichUtil.processContent(data) { containerView, text, range, rect in    let result = RichUtil.processClickText(data, range)    if let r = self.nicknameClickBlock{        r(result)    }} _: { containerView, text, range, rect in    let result = RichUtil.processClickText(data, range)    print(result)}

}

### 好友

class UserController: BaseTitleController {

var style:MyStyle!override func initViews() {    super.initViews()    initTableViewSafeArea()        tableView.register(TopicCell.self, forCellReuseIdentifier: Constant.CELL)}override func initDatum() {    super.initDatum()            if style == .friend || style == .select {        //好友        title = R.string.localizable.myFriend()    } else {        //粉丝        title = R.string.localizable.myFans()    }}override func viewWillAppear(_ animated: Bool) {    super.viewWillAppear(animated)        loadData()}func loadData() {    var api:Observable<ListResponse<User>>!        if style == .friend || style == .select  {        api = DefaultRepository.shared            .friends(PreferenceUtil.getUserId())    } else {        api = DefaultRepository.shared            .fans(PreferenceUtil.getUserId())    }        api.subscribeSuccess {[weak self] data in        self?.show(data.data?.data ?? [])    }.disposed(by: rx.disposeBag)}func show(_ data:[User]) {    datum.removeAll()        datum += data        tableView.reloadData()}static func start(_ controller:UINavigationController,_ style:MyStyle) {    let target = UserController()    target.style=style    controller.pushViewController(target, animated: true)}

}

//列表数据源
extension UserController{

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    let data = datum[indexPath.row] as! User        let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! TopicCell    cell.bind(data)        return cell}func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {    let data = datum[indexPath.row] as! User        if style == .select {        //抉择        SwiftEventBus.post(Constant.EVENT_USER_SELECTED, sender: data)                finish()    } else {        UserDetailController.start(navigationController!, id: data.id)    }}

}

## 视频和播放![在这里插入图片形容](https://img-blog.csdnimg.cn/a163b94f958d4e6d86867cbf1d223823.png)实在我的项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包含审核,转码,CDN,平安,播放器等,这里用不到这么多功能,所以应用了第三方播放器播放一般mp4,这应用饺子播放器框架。

func play(_ data:Video) {

//不开防盗链let model = SuperPlayerModel()//播放腾讯云视频// 配置 AppId

// model.appId = 0;
//
// model.videoId = [[SuperPlayerVideoId alloc] init];
// model.videoId.fileId = "5285890799710670616"; // 配置 FileId

//进行播放playerView.removeVideo()//间接应用url播放model.videoURL = data.uri.absoluteUri()playerView.play(with: model)//设置题目playerView.controlView.title = data.title

}

## 用户详情/更改材料![在这里插入图片形容](https://img-blog.csdnimg.cn/132a10deb54344368dc78aa13700829d.png)用户详情顶部显示用户信息,好友数量,上面别离显示创立的歌单,珍藏的歌单,公布的动静,相似微信朋友圈,右上角能够更改用户材料;应用第三方框架外面的kJXPagingListRefreshView控件实现。

func initUI() {

container.removeSubviews()//头部控件userHeaderView = UserDetailHeaderView()userHeaderView.followView.addTarget(self, action: #selector(followClick), for: .touchUpInside)userHeaderView.sendMessageView.addTarget(self, action: #selector(sendClick), for: .touchUpInside)//指示器indicatorView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: UserDetailController.SIZE_INDICATOR_HEIGHT))segmentedDataSource = JXSegmentedTitleDataSource()//题目segmentedDataSource.titles = [R.string.localizable.sheet(), R.string.localizable.feed()]//抉择的色彩segmentedDataSource.titleSelectedColor = .colorPrimary//默认色彩segmentedDataSource.titleNormalColor = .colorOnSurface//选中是否放大segmentedDataSource.isTitleZoomEnabled = falseindicatorView.dataSource=segmentedDataSourceindicatorView.backgroundColor = .clearindicatorView.delegate = self//指示器上面那条线let lineView = JXSegmentedIndicatorLineView()//选中色彩lineView.indicatorColor = .colorPrimarylineView.indicatorWidth = 30indicatorView.indicators = [lineView]pagerView = JXPagingListRefreshView(delegate: self)pagerView.mainTableView.gestureDelegate = selfpagerView.tg_width.equal(.fill)pagerView.tg_height.equal(.fill)container.addSubview(pagerView)indicatorView.listContainer = pagerView.listContainerView//扣边返回解决,上面的代码要加上pagerView.listContainerView.scrollView.panGestureRecognizer.require(toFail: self.navigationController!.interactivePopGestureRecognizer!)pagerView.mainTableView.panGestureRecognizer.require(toFail: self.navigationController!.interactivePopGestureRecognizer!)

}

而后就是把每个子界面放到独自View中,并在代理办法返回就行了。## 公布动静/抉择地位/门路布局![在这里插入图片形容](https://img-blog.csdnimg.cn/f7ad8f5367c64daf87281e11354317c3.png)公布成果和微信朋友圈相似,能够抉择图片,和地理位置;地理位置应用高德地图实现抉择,门路布局是调用零碎中装置的地图,相似微信。### 地位

/// 搜寻该地位的poi,不便用户抉择,也不便其他人找
func searchPOI() {

if keyword != nil {    //关键字搜寻    let request = AMapPOIKeywordsSearchRequest()        //关键字    request.keywords=keyword    //间隔排序    request.sortrule = 0    //是否返回扩大信息    request.requireExtension=true    search.aMapPOIKeywordsSearch(request)} else {    //搜寻地位左近    let request = AMapPOIAroundSearchRequest()    request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate!.latitude), longitude: CGFloat(coordinate!.longitude))        //间隔排序    request.sortrule=0        //是否返回扩大信息    request.requireExtension=true        search.aMapPOIAroundSearch(request)}

}

### 地图门路布局

/// 高德地图门路布局
/// 官网文档:https://lbs.amap.com/api/amap...
static func amapPathPlan(title:String,latitude:Double,longitude:Double) {

let urlString = "iosamap://path?sourceApplication=云音乐&backScheme=weichat&dlat=\(latitude)&dlon=\(longitude)&dname=\(title)"SuperApplicationUtil.open(urlString)

}

## 聊天/离线推送![在这里插入图片形容](https://img-blog.csdnimg.cn/22e188ada9d843729a84077cd4bf7702.png)大部分实在我的项目中聊天都会抉择第三方商业级付费聊天服务,罕用的有腾讯云聊天,融云聊天,网易云聊天等,这里抉择融云聊天服务,应用步骤是先在服务端生成聊天Token,这里是登录后返回,而后客户端登录聊天服务器,而后设置音讯监听,发送音讯等。### 聊天服务器

/// 连贯聊天服务器
func connectChat(_ data:Session) {

RCIMClient.shared()    .connect(withToken: data.chatToken) { code in        //音讯数据库关上,能够进入到主页面        //因为咱们利用不是纯微信这样的利用,所以就不再这里才跳转到主界面    } success: { userId in        //连贯胜利    } error: { status in        if (status == .RC_CONN_TOKEN_INCORRECT) {            //从 APP 服务获取新 token,并重连        } else {            //无奈连贯到 IM 服务器,请依据相应的错误码作出对应解决        }        //因为咱们这个利用,不是相似微信那样纯聊天利用,所以聊天服务器连贯失败,也让进入利用        //实在我的项目中依照需要实现就行了        SuperToast.show(title: R.string.localizable.errorMessageLogin())    }

}

### 音讯监听

func onReceived(_ message: RCMessage!, left nLeft: Int32, object: Any!, offline: Bool, hasPackage: Bool) {

DispatchQueue.main.async {    if message.targetId == self.currentChatUserId || offline {        //正在和这个人聊天,或者离线音讯    } else {        //其余音讯显示到告诉栏        NotificationUtil.showMessage(message)    }    //发送音讯未读数扭转了告诉    NotificationCenter.default.post(name: NSNotification.Name(rawValue: ON_MESSAGE_COUNT_CHANGED), object: nil, userInfo: nil)    //发送音讯到告诉(这个告诉是,跨界面通信,不是显示到告诉栏)    NotificationCenter.default.post(name: NSNotification.Name(rawValue: ON_MESSAGE), object: nil, userInfo: [Constant.DATA:message])}

}

### 文本音讯发送图片等其余音讯也是差不多。

/// 发送文本音讯
func sendTextMessage() {

let result=contentInputView.text.trimmedif SuperStringUtil.isBlank(result) {    SuperToast.show(title: R.string.localizable.hintEnterMessage())    return}//1.结构文本音讯let param = RCTextMessage(content: result)!//2.将文本音讯发送进来RCIMClient.shared().sendMessage(.ConversationType_PRIVATE, targetId: id, content: param, pushContent: nil, pushData: MessageUtil.createPushData(MessageUtil.getContent(param), PreferenceUtil.getUserId())) { messageId in    print("message send success \(messageId)")    DispatchQueue.main.async {        //清空输入框        self.clearInput()    }    self.addMessage(RCIMClient.shared().getMessage(messageId))} error: { code, messageId in    print("message send fail \(messageId) \(code)")}

}

## 离线推送须要付费苹果开发者账户,先开启SDK离线推送,而后在苹果开发者后盾创立推送证书,配置到融云,最初在代码中解决告诉点击等。

@objc func notificationClick(_ notification:Notification) {

processPushClick()

}

/// 解决推送点击
func processPushClick() {

let data = Push.deserialize(from: AppDelegate.shared.notificationData!)!switch data.style {case Push.PUSH_STYLE_CHAT:    processChatMessageClick(data.message!)default:    break}AppDelegate.shared.notificationData = nil

}

/// 聊天音讯告诉点击
func processChatMessageClick(_ data:PushMessage) {

ChatController.start(navigationController!, data.userId)

}

override func viewDidAppear(_ animated: Bool) {

super.viewDidAppear(animated)//延时的目标是让以后界面显示进去当前,在查看//查看是否须要解决告诉点击DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {    if let _ = AppDelegate.shared.notificationData {        self.processPushClick()    }}

}

## 商城/订单/领取/购物车![在这里插入图片形容](https://img-blog.csdnimg.cn/d35de5953765419c93cb0a4b5532a7fd.png)![在这里插入图片形容](https://img-blog.csdnimg.cn/86ebe1174149491aa58425c1d1393b6e.png)学到这里,大家不能说相熟,那么看到下面的界面,那么大体要能实现进去。### 详情富文本

//详情
self.detailView = QMUITextView()
self.detailView.tg_width.equal(.fill)
self.detailView.tg_height.equal(.wrap)
self.detailView.delegate=self
self.detailView.isScrollEnabled=false
self.detailView.isEditable=false

//去除左右边距
self.detailView.textContainer.lineFragmentPadding = 0

//去除上下边距
self.detailView.textContainerInset = .zero
contentContainer.addSubview(detailView)

### 宝/微信领取客户端先集成微信,支付宝SDK,而后申请服务端获取领取信息,设置到SDK,最初就是解决领取后果。

/// 解决支付宝领取
func processAlipay(_ data:String) {

//支付宝官网开发文档:https://docs.open.alipay.com/204/105295/AlipaySDK.defaultService()    .payOrder(data, fromScheme: Config.ALIPAY_CALLBACK_SCHEME) { data in        //如果手机中没有装置支付宝客户端        //会跳转H5领取页面        //领取相干的信息会通过这个办法回调        //解决支付宝领取后果        self.processAlipayResult(data as! [String:Any])    }

}

/// 解决微信领取
func processWechat(_ data:WechatPay) {

//把服务端返回的参数//设置到对应的字段let request = PayReq()request.partnerId = data.partneridrequest.prepayId = data.prepayidrequest.nonceStr = data.noncestrrequest.timeStamp = UInt32(data.timestamp)!request.package = data.packagerequest.sign = data.signWXApi.send(request) { data in    print("PayController processWechat \(data)")}

}

### 领取后果

/// 解决支付宝领取后果
func processAlipayResult(_ data:[String:Any]) {

let resultStatus = data["resultStatus"] as! Stringif "9000" == resultStatus {    //本地领取胜利    //不能依赖本地领取后果    //肯定要以服务端为准    SuperToast.showLoading(title: R.string.localizable.hintPayWait())    checkPayStatus()    //这里就不依据服务端判断了    //购买胜利统计} else if "6001" == resultStatus {    //勾销了    showCancel()} else {    //领取失败    showPayFailedTip()}

}

## 我的项目总结总体来说我的项目性能还是很全的,还有一些小性能,例如:快捷方式等就不在贴代码了,但必定没发和原版比,置信大家只有做过程序员就能了解,毕竟原版是一个商业级我的项目,几十个人天天开发和保护,而且继续了几年了;不过恕我直言,当初的常见的音乐软件都太简单了,各种性能,不过都要恰饭,如同又能了解了。