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

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

新鲜出炉高仿网易云音乐 APP