Swift高仿iOS网易云音乐Moya+RxSwift+Kingfisher+MVC+MVVM
Posted 爱上学习啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift高仿iOS网易云音乐Moya+RxSwift+Kingfisher+MVC+MVVM相关的知识,希望对你有一定的参考价值。
效果
列文章目录
因为目录比较多,每次更新这里比较麻烦,所以推荐点击到主页,然后查看ios Swift云音乐专栏。
目简介
这是一个使用Swift(还有OC版本)语言,从0开发一个iOS平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识;主要是使用系统功能,流行的第三方框架,第三方服务,完成接近企业级商业级项目。
目功能点
隐私协议对话框
启动界面和动态处理权限
引导界面和广告
轮播图和侧滑菜单
首页复杂列表和列表排序
音乐播放和音乐列表管理
全局音乐控制条
桌面歌词和自定义样式
全局媒体控制中心
评论和回复评论
评论富文本点击
评论提醒人和话题
朋友圈动态列表和发布
高德地图定位和路径规划
阿里云OSS上传
视频播放和控制
QQ/微信登录和分享
商城/购物车\\微信\\支付宝支付
文本和图片聊天
消息离线推送
自动和手动检查更新
内存泄漏和优化
…
发环境概述
2022年7月开发完成的,所以全部都是最新的,平均每3年会重新制作,现在已经是第三版了。
Xcode 13.4
iOS 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
end
end
户协议对话框
使用自定义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
()
导界面
引导界面比较简单,就是多个图片可以左右滚动。
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
广告界面
实现图片广告和视频广告,广告数据是在首页是缓存到本地,目的是在启动界面加载更快,因为真实项目中,大部分项目启动页面广告时间一共就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 = .resizeAspectFill
view.layer.insertSublayer(playerLayer!, at: 0)
显示图片就是显示本地图片了,没什么难点,就不贴代码了。
首页/歌单详情/黑胶唱片界面
首页没有顶部是轮播图,然后是可以左右的菜单,接下来是热门歌单,推荐单曲,最后是首页排序模块;整体上使用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 = 3
weak 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(Swift高仿iOS网易云音乐Moya+RxSwift+Kingfisher+MVC+MVVM
OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM
OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM
OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM