对部分bug的处理以及搜索功能的初步搭建
@ -150,6 +150,12 @@
|
||||
CBEE8E342BEB16BB007DA798 /* MPPositive_PlayerSilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E332BEB16BB007DA798 /* MPPositive_PlayerSilder.swift */; };
|
||||
CBEE8E362BEB2604007DA798 /* MPPositive_PlayerLyricView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E352BEB2604007DA798 /* MPPositive_PlayerLyricView.swift */; };
|
||||
CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */; };
|
||||
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */; };
|
||||
CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */; };
|
||||
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */; };
|
||||
CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */; };
|
||||
CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */; };
|
||||
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -299,6 +305,12 @@
|
||||
CBEE8E332BEB16BB007DA798 /* MPPositive_PlayerSilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerSilder.swift; sourceTree = "<group>"; };
|
||||
CBEE8E352BEB2604007DA798 /* MPPositive_PlayerLyricView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLyricView.swift; sourceTree = "<group>"; };
|
||||
CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SongViewModel.swift; sourceTree = "<group>"; };
|
||||
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemTableViewCell.swift; sourceTree = "<group>"; };
|
||||
CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchSuggestions.swift; sourceTree = "<group>"; };
|
||||
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemModel.swift; sourceTree = "<group>"; };
|
||||
CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionListTableViewCell.swift; sourceTree = "<group>"; };
|
||||
CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultShowViewController.swift; sourceTree = "<group>"; };
|
||||
CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchResults.swift; sourceTree = "<group>"; };
|
||||
E2C6C85BFD4CD80DBA96D149 /* Pods-MusicPlayer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MusicPlayer.release.xcconfig"; path = "Target Support Files/Pods-MusicPlayer/Pods-MusicPlayer.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -646,6 +658,7 @@
|
||||
CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */,
|
||||
CBB5D31E2BDF711600CC333D /* MPPositive_SongItemModel.swift */,
|
||||
CBB75B0A2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift */,
|
||||
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@ -757,6 +770,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CB0918A02BD26B0A006D2B39 /* MPPositive_SearchViewController.swift */,
|
||||
CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */,
|
||||
);
|
||||
path = "Search(搜索页)";
|
||||
sourceTree = "<group>";
|
||||
@ -771,6 +785,8 @@
|
||||
CBCB50212BD118BB009760B3 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */,
|
||||
CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
@ -809,6 +825,8 @@
|
||||
CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */,
|
||||
CB5661282BE09D0500CFD014 /* MPPositive_JsonPlayer.swift */,
|
||||
CBB9F9DC2BEDCFEE008338DE /* MPPositive_JsonLyrics.swift */,
|
||||
CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */,
|
||||
CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */,
|
||||
);
|
||||
path = JsonStructs;
|
||||
sourceTree = "<group>";
|
||||
@ -1045,12 +1063,14 @@
|
||||
CBE1CB4C2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift in Sources */,
|
||||
CBEE8E362BEB2604007DA798 /* MPPositive_PlayerLyricView.swift in Sources */,
|
||||
CBCB4FF22BD11402009760B3 /* MPSideA_AboutViewController.swift in Sources */,
|
||||
CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */,
|
||||
CB0918A52BD26E16006D2B39 /* MPPositive_BottomShowView.swift in Sources */,
|
||||
CBB5D31F2BDF711600CC333D /* MPPositive_SongItemModel.swift in Sources */,
|
||||
CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */,
|
||||
CBCB4FEF2BD11402009760B3 /* MPSideA_NavigationController.swift in Sources */,
|
||||
CBCB35212BD7ACE900802900 /* MPPositive_JsonBrowse.swift in Sources */,
|
||||
CBCB4FF62BD11402009760B3 /* MPSideA_DeleteViewController.swift in Sources */,
|
||||
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */,
|
||||
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */,
|
||||
CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */,
|
||||
CBCB50122BD11402009760B3 /* MPSideA_Home_FirstListCollectionViewCell.swift in Sources */,
|
||||
@ -1070,6 +1090,7 @@
|
||||
CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */,
|
||||
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */,
|
||||
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */,
|
||||
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */,
|
||||
CBCB50042BD11402009760B3 /* MPSideA_HomeViewController.swift in Sources */,
|
||||
CB09189D2BD25F63006D2B39 /* MPPositive_CustomTabBarItem.swift in Sources */,
|
||||
CBCB4FFC2BD11402009760B3 /* MPSideA_RenameViewController.swift in Sources */,
|
||||
@ -1108,6 +1129,7 @@
|
||||
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */,
|
||||
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */,
|
||||
CBCB4FF82BD11402009760B3 /* MPSideA_MoreViewController.swift in Sources */,
|
||||
CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */,
|
||||
CBCB50142BD11402009760B3 /* MPSideA_Home_FourthListCollectionViewCell.swift in Sources */,
|
||||
CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */,
|
||||
CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */,
|
||||
@ -1123,6 +1145,7 @@
|
||||
009662372BB14A5A00FCA65F /* MusicPlayer.xcdatamodeld in Sources */,
|
||||
CB09189B2BD25F50006D2B39 /* MPPositive_CustomTabBarView.swift in Sources */,
|
||||
CBE1CB502BDE4CC500701D57 /* MPPositive_ListHeaderViewModel.swift in Sources */,
|
||||
CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */,
|
||||
CBCAFB5F2BB3C55500BC6520 /* DateTime.swift in Sources */,
|
||||
CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */,
|
||||
CBCC23532BEE596E004D7A57 /* MPPositive_PlayerListShowTableViewCell.swift in Sources */,
|
||||
@ -1140,6 +1163,7 @@
|
||||
CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */,
|
||||
CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */,
|
||||
CBCB4FF02BD11402009760B3 /* MPSideA_PresentationController.swift in Sources */,
|
||||
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */,
|
||||
CBC6874B2BC2B0710023ECA6 /* String.swift in Sources */,
|
||||
CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */,
|
||||
);
|
||||
|
||||
@ -49,6 +49,13 @@
|
||||
ReferencedContainer = "container:MusicPlayer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "NSZombieEnabled"
|
||||
value = "YES"
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
|
Before Width: | Height: | Size: 558 B After Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 993 B After Width: | Height: | Size: 469 B |
22
MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1597880487@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1597880487@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 405 B |
BIN
MusicPlayer/Assets.xcassets/Positive/Player/Player_Shuffle'logo.imageset/Group_1597880487@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 638 B |
22
MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1597880488@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1597880488@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 524 B |
BIN
MusicPlayer/Assets.xcassets/Positive/Player/Player_Single'logo.imageset/Group_1597880488@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 796 B |
@ -71,10 +71,18 @@ extension NotificationCenter{
|
||||
case positive_browses_reload
|
||||
///列表数据已更新
|
||||
case positive_list_reload
|
||||
///播放器状态变化
|
||||
case switch_player_status
|
||||
///弹出底部音乐模块
|
||||
case pup_bottom_show
|
||||
///弹出音乐播放器
|
||||
case pup_player_vc
|
||||
///播放器页面更新
|
||||
case positive_player_reload
|
||||
///用户切换播放器播放方式
|
||||
case player_type_switch
|
||||
///用户清空了歌单
|
||||
case player_delete_list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,4 +112,14 @@ func createLabel(_ text:String? = nil, font:UIFont, textColor:UIColor, textAlign
|
||||
label.numberOfLines = lines
|
||||
return label
|
||||
}
|
||||
|
||||
///根据播放器状态将按钮的图片进行切换
|
||||
func switchPlayTypeBtnIcon(_ btn:UIButton) {
|
||||
switch MP_PlayerManager.shared.getPlayType() {
|
||||
case .normal://列表播放图案
|
||||
btn.setBackgroundImage(UIImage(named: "List_NormolPlay'logo"), for: .normal)
|
||||
case .random://随机播放图案
|
||||
btn.setBackgroundImage(UIImage(named: "Player_Shuffle'logo"), for: .normal)
|
||||
case .single://单曲循环图案
|
||||
btn.setBackgroundImage(UIImage(named: "Player_Single'logo"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@ class MP_NetWorkManager: NSObject {
|
||||
private let next:String = "/next"
|
||||
///播放器接口
|
||||
private let player:String = "/player"
|
||||
///搜索建议接口
|
||||
private let suggestions:String = "/music/get_search_suggestions"
|
||||
///搜索接口
|
||||
private let search = "/search"
|
||||
//MARK: - 固定参数
|
||||
//访问数据(首次首页预览时获得)
|
||||
private var visitorData:String?
|
||||
@ -42,7 +46,7 @@ class MP_NetWorkManager: NSObject {
|
||||
return
|
||||
}
|
||||
//生成新参数
|
||||
var parameters:[String:Any] = [
|
||||
let parameters:[String:Any] = [
|
||||
"ctoken":continuation,
|
||||
"continuation":continuation,
|
||||
"type":"next",
|
||||
@ -127,6 +131,7 @@ class MP_NetWorkManager: NSObject {
|
||||
}
|
||||
//MARK: - API请求
|
||||
extension MP_NetWorkManager {
|
||||
//MARK: - 请求首页预览
|
||||
///向YouTubemusic请求预览/首页数据
|
||||
func requestBrowseDatas() {
|
||||
//实行串行异步队列,进行多次请求。由于第一次之后的请求都必须携带对应的continuation编码,所以串行队列。直到最后一次请求的continuation值为空,销毁队列
|
||||
@ -193,6 +198,7 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - 请求列表专辑预览
|
||||
/// 向YouTubemusic请求列表/专辑数据,该接口调用的同样是browse预览接口
|
||||
/// - Parameters:
|
||||
/// - item: 需要查看的模块
|
||||
@ -205,7 +211,7 @@ extension MP_NetWorkManager {
|
||||
return
|
||||
}
|
||||
//设置参数,browseId与params参数是必定携带内容
|
||||
var parameters:[String:Any] = [
|
||||
let parameters:[String:Any] = [
|
||||
"browseId":(item.browseItem.browseContent.browseId ?? ""),
|
||||
"params":(item.browseItem.browseContent.params ?? ""),
|
||||
"prettyPrint":"false",
|
||||
@ -213,6 +219,7 @@ extension MP_NetWorkManager {
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
@ -255,6 +262,8 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 请求列表专辑下一部分
|
||||
///请求Next列表(优先于Player)
|
||||
/// - Parameter item: 请求的预览实体
|
||||
func requestNextList(_ item: MPPositive_BrowseItemViewModel, completion:@escaping(([MPPositive_SongItemModel]) -> Void)) {
|
||||
@ -266,7 +275,7 @@ extension MP_NetWorkManager {
|
||||
return
|
||||
}
|
||||
//设置参数,videoId与params参数是必定携带内容
|
||||
var parameters:[String:Any] = [
|
||||
let parameters:[String:Any] = [
|
||||
"playlistId":(item.browseItem.musicVideo.playListId ?? ""),
|
||||
"videoId":(item.browseItem.musicVideo.videoId ?? ""),
|
||||
"prettyPrint":"false",
|
||||
@ -274,6 +283,7 @@ extension MP_NetWorkManager {
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
@ -290,39 +300,6 @@ extension MP_NetWorkManager {
|
||||
completion(listSongs)
|
||||
}
|
||||
}
|
||||
///请求Next歌词/相关内容
|
||||
/// - Parameter item: 请求的预览实体
|
||||
func requestNextLyricsAndRelated(_ item: MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) {
|
||||
//拼接出next路径
|
||||
let path = header+point+next
|
||||
//设置url
|
||||
guard let url = URL(string: path) else {
|
||||
print("Url is Incorrect")
|
||||
return
|
||||
}
|
||||
//设置参数,videoId与params参数是必定携带内容
|
||||
var parameters:[String:Any] = [
|
||||
"videoId":(item.videoId ?? ""),
|
||||
"prettyPrint":"false",
|
||||
"context":[
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"hl":Language_first_local,
|
||||
//地址
|
||||
"gl":Location_First
|
||||
]
|
||||
]
|
||||
]
|
||||
//发送next列表歌词/相关内容请求
|
||||
requestPostNextLyricsAndRelated(url, parameters: parameters) { result in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
//请求next列表
|
||||
private func requestPostNextList(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SongItemModel]) -> Void)) {
|
||||
//发送post请求
|
||||
@ -339,6 +316,40 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
///请求Next歌词/相关内容
|
||||
/// - Parameter item: 请求的预览实体
|
||||
func requestNextLyricsAndRelated(_ item: MPPositive_SongItemModel, completion:@escaping(((String?,String?)) -> Void)) {
|
||||
//拼接出next路径
|
||||
let path = header+point+next
|
||||
//设置url
|
||||
guard let url = URL(string: path) else {
|
||||
print("Url is Incorrect")
|
||||
return
|
||||
}
|
||||
//设置参数,videoId与params参数是必定携带内容
|
||||
let parameters:[String:Any] = [
|
||||
"videoId":(item.videoId ?? ""),
|
||||
"prettyPrint":"false",
|
||||
"context":[
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"hl":Language_first_local,
|
||||
//地址
|
||||
"gl":Location_First
|
||||
]
|
||||
]
|
||||
]
|
||||
//发送next列表歌词/相关内容请求
|
||||
requestPostNextLyricsAndRelated(url, parameters: parameters) { result in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
//请求请求Next歌词/相关内容
|
||||
private func requestPostNextLyricsAndRelated(_ url:URL, parameters:Parameters, completion:@escaping(((String?,String?)) -> Void)) {
|
||||
//发送post请求
|
||||
@ -355,6 +366,8 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 请求player播放资源
|
||||
/// 请求Player(单曲/视频)播放资源
|
||||
/// - Parameter item: 请求的预览实体
|
||||
func requestPlayer(_ item: MPPositive_SongItemModel, completion:@escaping (([String]?, [String]?) -> Void)){
|
||||
@ -406,7 +419,7 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 请求歌词
|
||||
/// 请求歌词
|
||||
/// - Parameter lyricId: 歌词id
|
||||
func requestLyric(_ lyricId:String, completion:@escaping((String) -> Void)) {
|
||||
@ -418,13 +431,14 @@ extension MP_NetWorkManager {
|
||||
return
|
||||
}
|
||||
//设置参数,browseId与params参数是必定携带内容
|
||||
var parameters:[String:Any] = [
|
||||
let parameters:[String:Any] = [
|
||||
"browseId":lyricId,
|
||||
"prettyPrint":"false",
|
||||
"context":[
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
@ -453,6 +467,105 @@ extension MP_NetWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 请求搜索建议
|
||||
/// 请求搜索建议
|
||||
/// - Parameter content: 用户输入的文本
|
||||
func requestSearchSuggestions(_ content:String, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) {
|
||||
//拼接路径
|
||||
let path = header+point+suggestions
|
||||
//设置url
|
||||
guard let url = URL(string: path) else {
|
||||
print("Url is Incorrect")
|
||||
return
|
||||
}
|
||||
//设置参数
|
||||
let parameters:[String:Any] = [
|
||||
"input":content,
|
||||
"prettyPrint":"false",
|
||||
"context":[
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"hl":Language_first_local,
|
||||
//地址
|
||||
"gl":Location_First
|
||||
]
|
||||
]
|
||||
]
|
||||
requestPostSearchSuggestions(url, parameters: parameters) { result in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
//请求搜索建议
|
||||
private func requestPostSearchSuggestions(_ url:URL, parameters:Parameters, completion:@escaping(([[MPPositive_SearchSuggestionItemModel]]) -> Void)) {
|
||||
//发送post请求
|
||||
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchSuggestions.self) { [weak self] (response) in
|
||||
guard let self = self else {return}
|
||||
switch response.result {
|
||||
case .success(let value):
|
||||
parsingSearchSuggestions(value) { results in
|
||||
completion(results)
|
||||
}
|
||||
case .failure(let error):
|
||||
// 请求失败,处理错误
|
||||
print("Request failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求搜索结果
|
||||
/// - Parameter text: 用户请求文本
|
||||
func requestSearchResults(_ text:String) {
|
||||
//拼接路径
|
||||
let path = header+point+search
|
||||
//设置url
|
||||
guard let url = URL(string: path) else {
|
||||
print("Url is Incorrect")
|
||||
return
|
||||
}
|
||||
//设置参数
|
||||
let parameters:[String:Any] = [
|
||||
"query":text,
|
||||
"prettyPrint":"false",
|
||||
"context":[
|
||||
"client":[
|
||||
//web端
|
||||
"clientName": "WEB_REMIX",
|
||||
"visitorData":visitorData,
|
||||
//当前访问版本(日期值)
|
||||
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||
"platform":"DESKTOP",
|
||||
//语言
|
||||
"hl":Language_first_local,
|
||||
//地址
|
||||
"gl":Location_First
|
||||
]
|
||||
]
|
||||
]
|
||||
//
|
||||
}
|
||||
//请求搜索结果
|
||||
private func requestPostSearchResults(_ url:URL, parameters:Parameters) {
|
||||
//发送post请求
|
||||
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchResults.self) { [weak self] (response) in
|
||||
guard let self = self else {return}
|
||||
switch response.result {
|
||||
case .success(let value):
|
||||
if let contents = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents {
|
||||
parsingSearchResults(contents)
|
||||
}
|
||||
case .failure(let error):
|
||||
// 请求失败,处理错误
|
||||
print("Request failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - 数据解析
|
||||
extension MP_NetWorkManager {
|
||||
@ -581,16 +694,18 @@ extension MP_NetWorkManager {
|
||||
if let tab = tabs.first {
|
||||
//获取一张播放列表
|
||||
for (index, content) in (tab.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents ?? []).enumerated() {
|
||||
//生成一个音乐实体,用来装填部分数据
|
||||
let song = MPPositive_SongItemModel()
|
||||
song.index = index
|
||||
song.title = content.playlistPanelVideoRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.longBylineText = content.playlistPanelVideoRenderer?.longBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.lengthText = content.playlistPanelVideoRenderer?.lengthText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.shortBylineText = content.playlistPanelVideoRenderer?.shortBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.reviewUrls = content.playlistPanelVideoRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||
song.videoId = content.playlistPanelVideoRenderer?.videoId
|
||||
array.append(song)
|
||||
if let playlistPanelVideoRenderer = content.playlistPanelVideoRenderer {
|
||||
//生成一个音乐实体,用来装填部分数据
|
||||
let song = MPPositive_SongItemModel()
|
||||
song.index = index
|
||||
song.title = playlistPanelVideoRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.longBylineText = playlistPanelVideoRenderer.longBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.lengthText = playlistPanelVideoRenderer.lengthText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.shortBylineText = playlistPanelVideoRenderer.shortBylineText?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
song.reviewUrls = playlistPanelVideoRenderer.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||
song.videoId = playlistPanelVideoRenderer.videoId
|
||||
array.append(song)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -713,6 +828,60 @@ extension MP_NetWorkManager {
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 解析搜索建议_SearchSuggestions
|
||||
/// - Parameters:
|
||||
/// - searchSuggestions: 需要解析搜索建议
|
||||
/// - completion: 回掉两组搜索建议组
|
||||
private func parsingSearchSuggestions(_ searchSuggestions:JsonSearchSuggestions, completion:@escaping([[MPPositive_SearchSuggestionItemModel]]) -> Void) {
|
||||
if let contents = searchSuggestions.contents {
|
||||
var sections:[[MPPositive_SearchSuggestionItemModel]] = []
|
||||
contents.forEach { section in
|
||||
var suggestions:[MPPositive_SearchSuggestionItemModel] = []
|
||||
section.searchSuggestionsSectionRenderer?.contents?.forEach({ content in
|
||||
//生成搜索建议模型
|
||||
let item = MPPositive_SearchSuggestionItemModel()
|
||||
if let searchSuggestionRenderer = content.searchSuggestionRenderer {
|
||||
//这里是搜索词条建议
|
||||
item.title = searchSuggestionRenderer.suggestion?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
}else if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer {
|
||||
var reviewUrls:[String] = []
|
||||
//这里是搜索音乐建议
|
||||
musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.forEach({ thumbnail in
|
||||
reviewUrls.append(thumbnail.url ?? "")
|
||||
})
|
||||
item.reviewUrls = reviewUrls
|
||||
if let flexColumns = musicResponsiveListItemRenderer.flexColumns {
|
||||
for (index,flexColumn) in flexColumns.enumerated() {
|
||||
if index == 0 {
|
||||
//主标题
|
||||
item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||
}else {
|
||||
//副标题
|
||||
item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestions.append(item)
|
||||
})
|
||||
sections.append(suggestions)
|
||||
}
|
||||
completion(sections)
|
||||
}
|
||||
}
|
||||
///解析搜索结果_SearchResults
|
||||
private func parsingSearchResults(_ contents:[JsonSearchResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) {
|
||||
contents.forEach { content in
|
||||
//判断当前模块是最佳结果还是其它模块
|
||||
if let musicCardShelfRenderer = content.musicCardShelfRenderer {
|
||||
//当前是最佳结果
|
||||
|
||||
}else {
|
||||
//当前是其他结果
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - 解析具体内容形式
|
||||
//解析musicResponsiveListItemRenderer(单曲/视频)
|
||||
private func parsingMusicResponsiveListItemRenderer(_ musicResponsiveListItemRenderer: RootMusicResponsiveListItemRenderer) -> MPPositive_BrowseItemModel {
|
||||
@ -742,12 +911,6 @@ extension MP_NetWorkManager {
|
||||
browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
}
|
||||
// if run.navigationEndpoint?.browseEndpoint?.browseId != nil {
|
||||
// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
// }
|
||||
// if run.navigationEndpoint?.browseEndpoint?.params != nil {
|
||||
// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
// }
|
||||
})
|
||||
item.browseContent = browseContent
|
||||
}
|
||||
@ -785,12 +948,6 @@ extension MP_NetWorkManager {
|
||||
browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
}
|
||||
// if run.navigationEndpoint?.browseEndpoint?.browseId != nil {
|
||||
// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
// }
|
||||
// if run.navigationEndpoint?.browseEndpoint?.params != nil {
|
||||
// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
// }
|
||||
})
|
||||
musicTwoRowItemRenderer.subtitle?.runs?.forEach({ run in
|
||||
if run.navigationEndpoint?.browseEndpoint?.browseId != nil {
|
||||
@ -803,12 +960,6 @@ extension MP_NetWorkManager {
|
||||
browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
}
|
||||
// if run.navigationEndpoint?.browseEndpoint?.browseId != nil {
|
||||
// browseContent.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||
// }
|
||||
// if run.navigationEndpoint?.browseEndpoint?.params != nil {
|
||||
// browseContent.params = run.navigationEndpoint?.browseEndpoint?.params
|
||||
// }
|
||||
})
|
||||
item.browseContent = browseContent
|
||||
if let playListId = musicTwoRowItemRenderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint?.playlistId {
|
||||
|
||||
@ -43,21 +43,60 @@ typealias MP_PlayTimerStopAction = () -> Void
|
||||
///播放器调整进度时执行事件
|
||||
typealias MP_PlayTimerEditEndAction = () -> Void
|
||||
///播放器
|
||||
class MP_PlayerManager{
|
||||
class MP_PlayerManager:NSObject{
|
||||
///控制器单例
|
||||
static let shared = MP_PlayerManager()
|
||||
///播放器
|
||||
private var player:AVPlayer = AVPlayer()
|
||||
///load模块
|
||||
var loadPlayer:MPPositive_PlayerLoadViewModel!
|
||||
var loadPlayer:MPPositive_PlayerLoadViewModel!{
|
||||
didSet{
|
||||
if loadPlayer != nil {
|
||||
//当load模块接受到新值的时候,发出通知,提醒底部模块状态切换
|
||||
NotificationCenter.notificationKey.post(notificationName: .pup_bottom_show)
|
||||
}else {
|
||||
//用户清空了load模块,隐藏播放器
|
||||
NotificationCenter.notificationKey.post(notificationName: .player_delete_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
//当前播放器状态
|
||||
private var playState:MP_PlayerStateType = .Null
|
||||
private var playState:MP_PlayerStateType = .Null{
|
||||
didSet{
|
||||
//当播放器状态发生变化时,对播放器按钮状态进行切换
|
||||
NotificationCenter.notificationKey.post(notificationName: .switch_player_status, object: playState)
|
||||
}
|
||||
}
|
||||
///获取播放器播放状态
|
||||
func getPlayState() -> MP_PlayerStateType {
|
||||
return playState
|
||||
}
|
||||
///当前播放器播放方法
|
||||
private var playType:MP_PlayerPlayType = .normal{
|
||||
didSet{
|
||||
//当播放器播放方式变化后,发出通知
|
||||
NotificationCenter.notificationKey.post(notificationName: .player_type_switch)
|
||||
}
|
||||
}
|
||||
///获取播放器播放方法
|
||||
func getPlayType() -> MP_PlayerPlayType {
|
||||
return playType
|
||||
}
|
||||
/// 设置播放器播放方式
|
||||
/// - Parameter type: 新的类型
|
||||
func setPlayType(_ type:MP_PlayerPlayType) {
|
||||
playType = type
|
||||
if playType == .random {
|
||||
|
||||
}
|
||||
}
|
||||
///播放器启动时执行事件记录
|
||||
private var startActionBlock:MP_PlayTimerStartAction!
|
||||
///播放器运行时执行事件记录
|
||||
private var runActionBlock:MP_PlayTimerRunAction!
|
||||
var runActionBlock:MP_PlayTimerRunAction!
|
||||
|
||||
private init() {
|
||||
private override init() {
|
||||
super.init()
|
||||
// 添加观察者,监听播放结束事件
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(userSwitchCurrentVideoAction(_ :)), notificationName: .positive_player_reload)
|
||||
@ -65,16 +104,17 @@ class MP_PlayerManager{
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
///获取播放器播放状态
|
||||
func getPlayState() -> MP_PlayerStateType {
|
||||
return playState
|
||||
}
|
||||
/// 开始播放音乐
|
||||
/// - Parameters:
|
||||
/// - startAction: 开始播放时需要执行的事件
|
||||
/// - runAction: 播放途中需要执行的事件
|
||||
/// - endAction: 结束播放时需要执行的事件
|
||||
func play(startAction:MP_PlayTimerStartAction? = nil, runAction:MP_PlayTimerRunAction? = nil) {
|
||||
func play(startAction:MP_PlayTimerStartAction? = nil) {
|
||||
guard loadPlayer != nil, loadPlayer.currentVideo != nil else {
|
||||
//当两项数据皆为空时,播放器无法播放
|
||||
print("Player No Data")
|
||||
return
|
||||
}
|
||||
//检索播放器状态
|
||||
switch playState {
|
||||
case .Null://未启动
|
||||
@ -88,10 +128,7 @@ class MP_PlayerManager{
|
||||
if startAction != nil {
|
||||
startActionBlock = startAction
|
||||
}
|
||||
if runAction != nil {
|
||||
runActionBlock = runAction
|
||||
}
|
||||
//判断是否有PlayerItem
|
||||
//覆盖播放器原有的playerItem
|
||||
player.replaceCurrentItem(with: loadPlayer.currentVideo.resourcePlayerItem)
|
||||
//将进度回归为0
|
||||
player.seek(to: .zero)
|
||||
@ -102,12 +139,6 @@ class MP_PlayerManager{
|
||||
guard let self = self else { return }
|
||||
//转化为当前播放进度秒值
|
||||
let currentDuration = CMTimeGetSeconds(time)
|
||||
//当current为0时执行开始事件
|
||||
if currentDuration == 0 {
|
||||
if startActionBlock != nil {
|
||||
startActionBlock!()
|
||||
}
|
||||
}
|
||||
//获取当前播放音乐资源的最大时间值
|
||||
let maxDuration = getMusicDuration()
|
||||
if maxDuration.isNaN == false {
|
||||
@ -120,17 +151,65 @@ class MP_PlayerManager{
|
||||
}
|
||||
}
|
||||
})
|
||||
//播放
|
||||
player.play()
|
||||
playState = .Playing
|
||||
//启动除了当前播放意外的预加载内容
|
||||
// let set = Set(loadPlayer.listViewVideos.filter({$0.index != loadPlayer.currentVideo.index}))
|
||||
// set.forEach { item in
|
||||
// if item.canBePreloaded() == false {
|
||||
// item.preloadPlayerItem()
|
||||
// }
|
||||
//判断当前Video是否完成预加载
|
||||
if loadPlayer.currentVideo.isPreloading == true {
|
||||
//已经完成了预加载
|
||||
print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")")
|
||||
player.play()
|
||||
playState = .Playing
|
||||
//执行开始播放闭包
|
||||
if startActionBlock != nil {
|
||||
startActionBlock!()
|
||||
}
|
||||
}else {
|
||||
//未完成预加载,通过KVO来准确控制播放
|
||||
//为这个currentVideo的resourcePlayerItem创建KVO,分别监听这个item的status,playbackLikelyToKeepUp
|
||||
loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
|
||||
loadPlayer.currentVideo.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
|
||||
}
|
||||
//启动除了当前播放Video以外的Item的预加载内容
|
||||
// for item in loadPlayer.listViewVideos where item.song.videoId != loadPlayer.currentVideo.song.videoId {
|
||||
// item.preloadPlayerItem()
|
||||
// }
|
||||
}
|
||||
//实现KVO监听
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let keyPath = keyPath else {
|
||||
return
|
||||
}
|
||||
//根据keyPath检索
|
||||
switch keyPath {
|
||||
case "status"://playerItem状态
|
||||
if let statuValue = change?[.newKey] as? Int, statuValue == AVPlayerItem.Status.readyToPlay.rawValue {
|
||||
//当statuVlaue值等于playerItem准备播放的值,说明已经准备好播放
|
||||
print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 已经准备好播放")
|
||||
}else {
|
||||
print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 未做好准备播放")
|
||||
//当不能播放时,调整内容,再次播放
|
||||
}
|
||||
case "playbackLikelyToKeepUp"://是否存在足够的数据开始播放
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
|
||||
//播放器已经加载足够的数据,能够支撑播放
|
||||
print("当前音乐-\(loadPlayer.currentVideo.title ?? "") 有足够的缓存来播放")
|
||||
//判断当前播放器是否在播放当前音乐中
|
||||
if playState != .Playing {
|
||||
//还未播放当前音乐,启动播放
|
||||
print("开始播放音乐-\(loadPlayer.currentVideo.title ?? "")")
|
||||
player.play()
|
||||
playState = .Playing
|
||||
//执行开始播放闭包
|
||||
if startActionBlock != nil {
|
||||
startActionBlock!()
|
||||
}
|
||||
}else {
|
||||
//播放器已经在播放了,不需要操作
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
//MARK: - 获取当前音乐总长度
|
||||
///获取音乐资源总时长
|
||||
private func getMusicDuration() -> TimeInterval {
|
||||
return CMTimeGetSeconds(player.currentItem?.duration ?? .zero)
|
||||
@ -143,8 +222,15 @@ class MP_PlayerManager{
|
||||
guard playState == .Playing else {
|
||||
return
|
||||
}
|
||||
//当前音乐播放器正在播放中,下一首
|
||||
nextEvent()
|
||||
switch playType {
|
||||
case .single:
|
||||
playState = .Null
|
||||
//重播
|
||||
player.seek(to: CMTime.zero)
|
||||
default:
|
||||
//当前音乐播放器正在播放中,下一首
|
||||
nextEvent()
|
||||
}
|
||||
}
|
||||
//MARK: - 暂停播放
|
||||
///内部暂停播放
|
||||
@ -211,7 +297,7 @@ class MP_PlayerManager{
|
||||
|
||||
//MARK: - 停止播放
|
||||
//停止播放
|
||||
private func stop() {
|
||||
func stop() {
|
||||
//检索播放状态,是否已启动
|
||||
guard playState != .Null else {
|
||||
//未启动
|
||||
@ -224,40 +310,101 @@ class MP_PlayerManager{
|
||||
//MARK: - 切歌(上一首/下一首)
|
||||
///上一首歌事件
|
||||
func previousEvent() {
|
||||
//判断是否存在上一首音乐
|
||||
let targetIndex = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo)
|
||||
if targetIndex == 0 {
|
||||
//当前音乐第一首,更新列表内容,获取最后一首歌,并播放
|
||||
let last = loadPlayer.songVideos.last
|
||||
loadPlayer.improveData(last?.videoId ?? "")
|
||||
}else {
|
||||
//存在上一首,获取上一首ID,并播放
|
||||
let song = loadPlayer.songVideos.first(where: {$0.index == (loadPlayer.currentVideo.index-1)})
|
||||
loadPlayer.improveData(song?.videoId ?? "")
|
||||
//将播放器状态调整未播放
|
||||
playState = .Null
|
||||
var nextIndex:Int = 0
|
||||
//判断当前音乐播放方式
|
||||
switch playType {
|
||||
case .random://随机,播放随机列表内容
|
||||
for (index, item) in loadPlayer.randomVideos.enumerated() {
|
||||
if item.videoId == loadPlayer.currentVideo.song.videoId {
|
||||
//找到播放音乐的索引
|
||||
nextIndex = index - 1
|
||||
}
|
||||
}
|
||||
//假如next为负数,则直接播放列表最后一首
|
||||
if nextIndex < 0 {
|
||||
//播放列表最后一首
|
||||
let last = loadPlayer.randomVideos.last
|
||||
loadPlayer.improveData(last?.videoId ?? "")
|
||||
}else {
|
||||
//查询列表对应单曲
|
||||
let song = loadPlayer.randomVideos[nextIndex]
|
||||
loadPlayer.improveData(song.videoId ?? "")
|
||||
}
|
||||
default://常规播放或者单曲播放
|
||||
for (index, item) in loadPlayer.songVideos.enumerated() {
|
||||
if item.videoId == loadPlayer.currentVideo.song.videoId {
|
||||
//找到播放音乐的索引
|
||||
nextIndex = index - 1
|
||||
}
|
||||
}
|
||||
//假如next为负数,则直接播放列表最后一首
|
||||
if nextIndex < 0 {
|
||||
//播放列表最后一首
|
||||
let last = loadPlayer.songVideos.last
|
||||
loadPlayer.improveData(last?.videoId ?? "")
|
||||
}else {
|
||||
//查询列表对应单曲
|
||||
let song = loadPlayer.songVideos[nextIndex]
|
||||
loadPlayer.improveData(song.videoId ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
///下一首歌事件
|
||||
func nextEvent() {
|
||||
//判断是否存在下一首音乐
|
||||
let targetIndex = loadPlayer.listViewVideos.firstIndex(of: loadPlayer.currentVideo)
|
||||
if targetIndex == (loadPlayer.listViewVideos.count - 1) {
|
||||
//当前音乐最后一首,更新列表内容,获取第一首歌,并播放
|
||||
let first = loadPlayer.songVideos.first
|
||||
loadPlayer.improveData(first?.videoId ?? "")
|
||||
}else {
|
||||
//存在下一首,获取下一首ID,并播放
|
||||
let song = loadPlayer.songVideos.first(where: {$0.index == (loadPlayer.currentVideo.index+1)})
|
||||
loadPlayer.improveData(song?.videoId ?? "")
|
||||
//将播放器状态调整未播放
|
||||
playState = .Null
|
||||
var nextIndex:Int = 0
|
||||
switch playType {
|
||||
case .random:
|
||||
for (index, item) in loadPlayer.randomVideos.enumerated() {
|
||||
if item.videoId == loadPlayer.currentVideo.song.videoId {
|
||||
//找到播放音乐的索引
|
||||
nextIndex = index + 1
|
||||
}
|
||||
}
|
||||
//超出播放列表数
|
||||
if nextIndex > (loadPlayer.randomVideos.count-1) {
|
||||
//播放列表第一首
|
||||
let first = loadPlayer.randomVideos.first
|
||||
loadPlayer.improveData(first?.videoId ?? "")
|
||||
}else {
|
||||
//存在下一首,获取下一首ID,并播放
|
||||
let song = loadPlayer.randomVideos[nextIndex]
|
||||
loadPlayer.improveData(song.videoId ?? "")
|
||||
}
|
||||
default:
|
||||
for (index, item) in loadPlayer.songVideos.enumerated() {
|
||||
if item.videoId == loadPlayer.currentVideo.song.videoId {
|
||||
//找到播放音乐的索引
|
||||
nextIndex = index + 1
|
||||
}
|
||||
}
|
||||
//超出播放列表数
|
||||
if nextIndex > (loadPlayer.songVideos.count-1) {
|
||||
//播放列表第一首
|
||||
let first = loadPlayer.songVideos.first
|
||||
loadPlayer.improveData(first?.videoId ?? "")
|
||||
}else {
|
||||
//存在下一首,获取下一首ID,并播放
|
||||
let song = loadPlayer.songVideos[nextIndex]
|
||||
loadPlayer.improveData(song.videoId ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
///监听到用户切换当前音乐
|
||||
@objc private func userSwitchCurrentVideoAction(_ sender:Notification) {
|
||||
//将播放器状态调整未播放
|
||||
playState = .Null
|
||||
//优先获取传递的值
|
||||
if let video = sender.object as? MPPositive_SongViewModel {
|
||||
video.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
|
||||
video.resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||
}
|
||||
if loadPlayer.currentVideo != nil {
|
||||
//开始播放
|
||||
play(startAction: startActionBlock,runAction: runActionBlock)
|
||||
}else {
|
||||
//用户删除了音乐,播放下一首音乐
|
||||
|
||||
play(startAction: startActionBlock)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,622 @@
|
||||
//
|
||||
// MPPositive_JsonSearchResults.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
///搜索结果结构
|
||||
struct JsonSearchResults: Codable {
|
||||
let contents:Contents?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
contents = try values.decodeIfPresent(Contents.self, forKey: .contents)
|
||||
}
|
||||
struct Contents: Codable {
|
||||
let tabbedSearchResultsRenderer:TabbedSearchResultsRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tabbedSearchResultsRenderer = "tabbedSearchResultsRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
tabbedSearchResultsRenderer = try values.decodeIfPresent(TabbedSearchResultsRenderer.self, forKey: .tabbedSearchResultsRenderer)
|
||||
}
|
||||
struct TabbedSearchResultsRenderer: Codable {
|
||||
let tabs:[Tab]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tabs = "tabs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
tabs = try values.decodeIfPresent([Tab].self, forKey: .tabs)
|
||||
}
|
||||
struct Tab:Codable {
|
||||
let tabRenderer:TabRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case tabRenderer = "tabRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
tabRenderer = try values.decodeIfPresent(TabRenderer.self, forKey: .tabRenderer)
|
||||
}
|
||||
struct TabRenderer: Codable {
|
||||
let content:Content?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case content = "content"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
content = try values.decodeIfPresent(Content.self, forKey: .content)
|
||||
}
|
||||
struct Content: Codable {
|
||||
let sectionListRenderer:SectionListRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case sectionListRenderer = "sectionListRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
sectionListRenderer = try values.decodeIfPresent(SectionListRenderer.self, forKey: .sectionListRenderer)
|
||||
}
|
||||
struct SectionListRenderer: Codable {
|
||||
///模块(每个模块内容不一样)
|
||||
let contents:[Content]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||
}
|
||||
//MARK: - 模块
|
||||
struct Content: Codable {
|
||||
///最佳结果
|
||||
let musicCardShelfRenderer:MusicCardShelfRenderer?
|
||||
///其他结果
|
||||
let musicShelfRenderer:MusicShelfRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicCardShelfRenderer = "musicCardShelfRenderer"
|
||||
case musicShelfRenderer = "musicShelfRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicCardShelfRenderer = try values.decodeIfPresent(MusicCardShelfRenderer.self, forKey: .musicCardShelfRenderer)
|
||||
musicShelfRenderer = try values.decodeIfPresent(MusicShelfRenderer.self, forKey: .musicShelfRenderer)
|
||||
}
|
||||
//MARK: - 不同模块携带的内容
|
||||
///最佳结果
|
||||
struct MusicCardShelfRenderer: Codable {
|
||||
///最佳结果封面
|
||||
let thumbnail:Thumbnail?
|
||||
///最佳结果标题(附带单曲ID)
|
||||
let title:Title?
|
||||
///最佳结果副标题
|
||||
let subtitle:Subtitle?
|
||||
///最佳结果组头标题
|
||||
let header:Header?
|
||||
///最佳结果其他内容,第0位是无用数据,获取时跳过
|
||||
let contents:[Content]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
case title = "title"
|
||||
case subtitle = "subtitle"
|
||||
case header = "header"
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
title = try values.decodeIfPresent(Title.self, forKey: .title)
|
||||
subtitle = try values.decodeIfPresent(Subtitle.self, forKey: .subtitle)
|
||||
header = try values.decodeIfPresent(Header.self, forKey: .header)
|
||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||
}
|
||||
///最佳结果封面
|
||||
struct Thumbnail: Codable {
|
||||
let musicThumbnailRenderer:MusicThumbnailRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicThumbnailRenderer = "musicThumbnailRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer)
|
||||
}
|
||||
struct MusicThumbnailRenderer: Codable {
|
||||
let thumbnail:Thumbnail?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let thumbnails:[Thumbnails]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnails = "thumbnails"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails)
|
||||
}
|
||||
struct Thumbnails: Codable {
|
||||
let url:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
url = try values.decodeIfPresent(String.self, forKey: .url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///最佳结果标题
|
||||
struct Title: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
let navigationEndpoint:NavigationEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
case navigationEndpoint = "navigationEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint)
|
||||
}
|
||||
struct NavigationEndpoint: Codable {
|
||||
///这个值存在就是音乐单曲
|
||||
let watchEndpoint:WatchEndpoint?
|
||||
///这个值存在就是艺术家
|
||||
let browseEndpoint:BrowseEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case watchEndpoint = "watchEndpoint"
|
||||
case browseEndpoint = "browseEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
watchEndpoint = try values.decodeIfPresent(WatchEndpoint.self, forKey: .watchEndpoint)
|
||||
browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint)
|
||||
}
|
||||
struct WatchEndpoint: Codable {
|
||||
let videoId:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case videoId = "videoId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
videoId = try values.decodeIfPresent(String.self, forKey: .videoId)
|
||||
}
|
||||
}
|
||||
struct BrowseEndpoint: Codable {
|
||||
let browseId:String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case browseId = "browseId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///最佳结果副标题
|
||||
struct Subtitle: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
///最佳结果组头
|
||||
struct Header: Codable {
|
||||
let musicCardShelfHeaderBasicRenderer:MusicCardShelfHeaderBasicRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicCardShelfHeaderBasicRenderer = "musicCardShelfHeaderBasicRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicCardShelfHeaderBasicRenderer = try values.decodeIfPresent(MusicCardShelfHeaderBasicRenderer.self, forKey: .musicCardShelfHeaderBasicRenderer)
|
||||
}
|
||||
struct MusicCardShelfHeaderBasicRenderer: Codable {
|
||||
let title:Title?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title = "title"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
title = try values.decodeIfPresent(Title.self, forKey: .title)
|
||||
}
|
||||
struct Title:Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///最佳结果其他内容
|
||||
struct Content: Codable {
|
||||
let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer)
|
||||
}
|
||||
struct MusicResponsiveListItemRenderer: Codable {
|
||||
///封面
|
||||
let thumbnail:Thumbnail?
|
||||
///文本内容(第0位是标题,其他拼成副标题)
|
||||
let flexColumns:[FlexColumn]?
|
||||
///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频)
|
||||
let playlistItemData:PlaylistItemData?
|
||||
///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单)
|
||||
let navigationEndpoint:NavigationEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
case flexColumns = "flexColumns"
|
||||
case playlistItemData = "playlistItemData"
|
||||
case navigationEndpoint = "navigationEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns)
|
||||
playlistItemData = try values.decodeIfPresent(PlaylistItemData.self, forKey: .playlistItemData)
|
||||
navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let musicThumbnailRenderer:MusicThumbnailRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicThumbnailRenderer = "musicThumbnailRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer)
|
||||
}
|
||||
struct MusicThumbnailRenderer: Codable {
|
||||
let thumbnail:Thumbnail?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let thumbnails:[Thumbnails]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnails = "thumbnails"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails)
|
||||
}
|
||||
struct Thumbnails: Codable {
|
||||
let url:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
url = try values.decodeIfPresent(String.self, forKey: .url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct FlexColumn: Codable {
|
||||
let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer)
|
||||
}
|
||||
struct MusicResponsiveListItemFlexColumnRenderer: Codable {
|
||||
let text:Text?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(Text.self, forKey: .text)
|
||||
}
|
||||
struct Text: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct PlaylistItemData: Codable {
|
||||
let videoId:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case videoId = "videoId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
videoId = try values.decodeIfPresent(String.self, forKey: .videoId)
|
||||
}
|
||||
}
|
||||
struct NavigationEndpoint: Codable {
|
||||
let browseEndpoint:BrowseEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case browseEndpoint = "browseEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint)
|
||||
}
|
||||
struct BrowseEndpoint: Codable {
|
||||
let browseId:String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case browseId = "browseId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///其他结果
|
||||
struct MusicShelfRenderer: Codable {
|
||||
///模块标题
|
||||
let title:Title?
|
||||
///模块内容
|
||||
let contents:[Content]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title = "title"
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
title = try values.decodeIfPresent(Title.self, forKey: .title)
|
||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||
}
|
||||
struct Title:Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Content: Codable {
|
||||
let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer)
|
||||
}
|
||||
struct MusicResponsiveListItemRenderer: Codable {
|
||||
///封面
|
||||
let thumbnail:Thumbnail?
|
||||
///文本内容(第0位是标题,其他拼成副标题)
|
||||
let flexColumns:[FlexColumn]?
|
||||
///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频)
|
||||
let playlistItemData:PlaylistItemData?
|
||||
///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单)
|
||||
let navigationEndpoint:NavigationEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
case flexColumns = "flexColumns"
|
||||
case playlistItemData = "playlistItemData"
|
||||
case navigationEndpoint = "navigationEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns)
|
||||
playlistItemData = try values.decodeIfPresent(PlaylistItemData.self, forKey: .playlistItemData)
|
||||
navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let musicThumbnailRenderer:MusicThumbnailRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicThumbnailRenderer = "musicThumbnailRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer)
|
||||
}
|
||||
struct MusicThumbnailRenderer: Codable {
|
||||
let thumbnail:Thumbnail?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let thumbnails:[Thumbnails]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnails = "thumbnails"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails)
|
||||
}
|
||||
struct Thumbnails: Codable {
|
||||
let url:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
url = try values.decodeIfPresent(String.self, forKey: .url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct FlexColumn: Codable {
|
||||
let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer)
|
||||
}
|
||||
struct MusicResponsiveListItemFlexColumnRenderer: Codable {
|
||||
let text:Text?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(Text.self, forKey: .text)
|
||||
}
|
||||
struct Text: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
///标题文本
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct PlaylistItemData: Codable {
|
||||
let videoId:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case videoId = "videoId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
videoId = try values.decodeIfPresent(String.self, forKey: .videoId)
|
||||
}
|
||||
}
|
||||
struct NavigationEndpoint: Codable {
|
||||
let browseEndpoint:BrowseEndpoint?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case browseEndpoint = "browseEndpoint"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint)
|
||||
}
|
||||
struct BrowseEndpoint: Codable {
|
||||
let browseId:String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case browseId = "browseId"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
//
|
||||
// MPPositive_JsonSearchSuggestions.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct JsonSearchSuggestions: Codable {
|
||||
let contents:[Content]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||
}
|
||||
struct Content: Codable {
|
||||
let searchSuggestionsSectionRenderer:SearchSuggestionsSectionRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case searchSuggestionsSectionRenderer = "searchSuggestionsSectionRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
searchSuggestionsSectionRenderer = try values.decodeIfPresent(SearchSuggestionsSectionRenderer.self, forKey: .searchSuggestionsSectionRenderer)
|
||||
}
|
||||
struct SearchSuggestionsSectionRenderer: Codable {
|
||||
let contents:[Content]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case contents = "contents"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||
}
|
||||
struct Content: Codable {
|
||||
///词条建议
|
||||
let searchSuggestionRenderer:SearchSuggestionRenderer?
|
||||
///音乐建议
|
||||
let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case searchSuggestionRenderer = "searchSuggestionRenderer"
|
||||
case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
searchSuggestionRenderer = try values.decodeIfPresent(SearchSuggestionRenderer.self, forKey: .searchSuggestionRenderer)
|
||||
musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer)
|
||||
}
|
||||
//MARK: - 词条建议
|
||||
struct SearchSuggestionRenderer: Codable {
|
||||
let suggestion:Suggestion?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case suggestion = "suggestion"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
suggestion = try values.decodeIfPresent(Suggestion.self, forKey: .suggestion)
|
||||
}
|
||||
struct Suggestion: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - 音乐建议
|
||||
struct MusicResponsiveListItemRenderer: Codable {
|
||||
let thumbnail:Thumbnail?
|
||||
let flexColumns:[FlexColumn]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
case flexColumns = "flexColumns"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
let musicThumbnailRenderer:MusicThumbnailRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicThumbnailRenderer = "musicThumbnailRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer)
|
||||
}
|
||||
|
||||
struct MusicThumbnailRenderer: Codable {
|
||||
let thumbnail:Thumbnail?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnail = "thumbnail"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
}
|
||||
struct Thumbnail: Codable {
|
||||
///封面图片组(默认取最后一位,图像内容最大最高清)
|
||||
let thumbnails:[Thumbnails]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case thumbnails = "thumbnails"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails)
|
||||
}
|
||||
//MARK: - 封面图片
|
||||
///封面图片
|
||||
struct Thumbnails: Codable {
|
||||
///封面图片路径
|
||||
let url:String?
|
||||
let width:CGFloat?
|
||||
let height:CGFloat?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url = "url"
|
||||
case width = "width"
|
||||
case height = "height"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
url = try values.decodeIfPresent(String.self, forKey: .url)
|
||||
width = try values.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||
height = try values.decodeIfPresent(CGFloat.self, forKey: .height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct FlexColumn: Codable {
|
||||
let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer)
|
||||
}
|
||||
struct MusicResponsiveListItemFlexColumnRenderer: Codable {
|
||||
let text:Text?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(Text.self, forKey: .text)
|
||||
}
|
||||
struct Text: Codable {
|
||||
let runs:[Run]?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case runs = "runs"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||
}
|
||||
struct Run: Codable {
|
||||
let text:String?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text = "text"
|
||||
}
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// MPPositive_SearchSuggestionsModel.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
///搜索建议模型
|
||||
class MPPositive_SearchSuggestionItemModel: NSObject {
|
||||
///主标题
|
||||
var title:String?
|
||||
///副标题(音乐推荐才有,内容五花八门,非主题内容拼接在一起)
|
||||
var subtitle:String?
|
||||
///预览图路径组(音乐推荐才有)
|
||||
var reviewUrls:[String]?
|
||||
}
|
||||
@ -23,7 +23,7 @@ class MPPositive_SongViewModel: NSObject {
|
||||
var lyrics:String?
|
||||
///相关内容ID
|
||||
var relatedId:String?
|
||||
///是否完成本次预加载
|
||||
///是否完成预加载
|
||||
var isPreloading:Bool?
|
||||
///是否收藏
|
||||
var isCollection:Bool?
|
||||
@ -39,10 +39,9 @@ class MPPositive_SongViewModel: NSObject {
|
||||
configure()
|
||||
}
|
||||
deinit {
|
||||
if self.isKvo == true {
|
||||
// 移除观察者
|
||||
self.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
|
||||
self.isKvo = false
|
||||
if isKvo == true {
|
||||
resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||
isKvo = false
|
||||
}
|
||||
}
|
||||
//数据配置
|
||||
@ -83,46 +82,41 @@ class MPPositive_SongViewModel: NSObject {
|
||||
}
|
||||
//MARK: - 资源预加载
|
||||
//检测资源是否能被预加载
|
||||
func canBePreloaded() -> Bool {
|
||||
private func canBePreloaded() -> Bool {
|
||||
return self.isPreloading ?? false
|
||||
}
|
||||
//异步预加载
|
||||
///异步预加载
|
||||
func preloadPlayerItem() {
|
||||
if isKvo == false {
|
||||
//为playerItem添加监听
|
||||
self.resourcePlayerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
|
||||
isKvo = true
|
||||
//执行预加载
|
||||
guard canBePreloaded() == false else {
|
||||
print("\(title ?? "")已经预加载了")
|
||||
return
|
||||
}
|
||||
print(resourcePlayerItem.status)
|
||||
//手动触发,以此加载数据
|
||||
self.resourcePlayerItem.seek(to: .zero) {[weak self] _ in
|
||||
guard let self = self else {return}
|
||||
// 使用 dispatchSource 监听属性变化
|
||||
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .background))
|
||||
//一秒触发一次
|
||||
timer.schedule(deadline: .now(), repeating: .seconds(1))
|
||||
timer.setEventHandler{
|
||||
if self.resourcePlayerItem.isPlaybackLikelyToKeepUp {
|
||||
//预加载完成
|
||||
self.isPreloading = true
|
||||
if self.isKvo == true {
|
||||
// 当预加载足够时,移除观察者
|
||||
self.resourcePlayerItem.removeObserver(self, forKeyPath: "status")
|
||||
self.isKvo = false
|
||||
timer.cancel()
|
||||
}
|
||||
//实行异步监听预加载
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
if self.isKvo == false {
|
||||
//为playerItem添加监听
|
||||
self.resourcePlayerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
|
||||
self.isKvo = true
|
||||
}
|
||||
}
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "status" {
|
||||
if let status = change?[.newKey] as? Int, status == Int(AVPlayerItem.Status.readyToPlay.rawValue) {
|
||||
// 播放准备就绪
|
||||
print("\(self.title ?? "") is Ok")
|
||||
}else {
|
||||
//资源无法播放
|
||||
print("\(self.title ?? "") is bad")
|
||||
if keyPath == "playbackLikelyToKeepUp" {
|
||||
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool, playbackLikelyToKeepUp == true {
|
||||
//当前资源已经预加载到合适的程度,移除KVO监听
|
||||
print("\(title ?? "")预加载到合适的进度")
|
||||
if isKvo == true {
|
||||
resourcePlayerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
|
||||
isKvo = false
|
||||
}
|
||||
//表示已经完成了预加载
|
||||
isPreloading = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,14 +8,21 @@
|
||||
import UIKit
|
||||
///播放器管理ViewModel
|
||||
class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
/// 单曲列表
|
||||
/// 单曲常规列表
|
||||
var songVideos:[MPPositive_SongItemModel]!
|
||||
///随机播放列表
|
||||
var randomVideos:[MPPositive_SongItemModel]!
|
||||
///当前播放音乐ViewModel
|
||||
var currentVideo:MPPositive_SongViewModel!{
|
||||
didSet{
|
||||
if currentVideo != nil {
|
||||
//当值变化时通知播放器页面,更新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
|
||||
willSet{
|
||||
if newValue != nil {
|
||||
if currentVideo != nil {
|
||||
//当值变化时通知播放器页面,更新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload, object: currentVideo)
|
||||
}else {
|
||||
//当值变化时通知播放器页面,更新UI
|
||||
NotificationCenter.notificationKey.post(notificationName: .positive_player_reload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,35 +38,39 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
/// - firstVideoId: 需要播放的第一首歌
|
||||
init(_ songs:[MPPositive_SongItemModel], currentVideoId: String) {
|
||||
super.init()
|
||||
//清空数据
|
||||
self.songVideos = songs
|
||||
//根据列表生成一份随机播放列表
|
||||
self.randomVideos = self.songVideos.shuffled()
|
||||
self.listViewVideos = []
|
||||
self.currentVideoId = currentVideoId
|
||||
}
|
||||
|
||||
///将选中Video的上下2项包括本身总计3项Video进行补全转为ViewModel,并播放这首音乐
|
||||
func improveData(_ targetVideoId:String) {
|
||||
guard let targetVideo = self.songVideos.first(where: {$0.videoId == targetVideoId}) else {
|
||||
//获取targetVideoId的索引
|
||||
guard let targetIndex = self.songVideos.firstIndex(where: {$0.videoId == targetVideoId}) else {
|
||||
return
|
||||
}
|
||||
//对于选中Video的集合
|
||||
var array:[MPPositive_SongItemModel] = []
|
||||
array.append(self.songVideos[targetIndex])
|
||||
//获取上一位
|
||||
if let previous = self.songVideos.first(where: {$0.index == (targetVideo.index-1)}) {
|
||||
array.append(previous)
|
||||
let previousIndex = targetIndex-1
|
||||
if previousIndex >= 0 {
|
||||
array.append(self.songVideos[previousIndex])
|
||||
}
|
||||
array.append(targetVideo)
|
||||
//获取下一位
|
||||
if let next = self.songVideos.first(where: {$0.index == (targetVideo.index+1)}) {
|
||||
array.append(next)
|
||||
let nextIndex = targetIndex+1
|
||||
if nextIndex < songVideos.count {
|
||||
array.append(self.songVideos[nextIndex])
|
||||
}
|
||||
//获取完成,优先检索ViewModel,看看是否已存在补完video
|
||||
let videoIDs = Set(listViewVideos.map({$0.song.videoId}))
|
||||
//比较videoID,去掉已经补完的内容
|
||||
array = array.filter({!videoIDs.contains($0.videoId)})
|
||||
group = DispatchGroup()
|
||||
var numbers = 0
|
||||
//去重完毕,对剩下内容补完
|
||||
array.forEach { item in
|
||||
for item in array {
|
||||
group?.enter()
|
||||
//补全歌词id和相关内容id
|
||||
improveDataforLycirsAndRelated(item) {[weak self] (result) in
|
||||
@ -82,41 +93,28 @@ class MPPositive_PlayerLoadViewModel: NSObject {
|
||||
self.listViewVideos = self.listViewVideos.sorted(by: {$0.index < $1.index})
|
||||
//排序完成,确定播放音乐
|
||||
self.currentVideo = self.listViewVideos.first(where: {$0.song.videoId == targetVideoId})
|
||||
|
||||
self.group = nil
|
||||
})
|
||||
}
|
||||
///移除选中的song,并更新listViewVideos,移除相同index的值
|
||||
func removeData(_ targetVideoId:String) {
|
||||
let targetIndex = songVideos.firstIndex(where: {$0.videoId == targetVideoId})
|
||||
let targetIndex = songVideos.firstIndex(where: {$0.videoId == targetVideoId}) ?? 0
|
||||
//将选中的音乐移除,同时更新listView
|
||||
songVideos = songVideos.filter({$0.videoId != targetVideoId})
|
||||
randomVideos = randomVideos.filter({$0.videoId != targetVideoId})
|
||||
listViewVideos = listViewVideos.filter({$0.song.videoId != targetVideoId})
|
||||
//更新下标/索引
|
||||
for (index, item) in songVideos.enumerated() {
|
||||
item.index = index
|
||||
}
|
||||
listViewVideos.forEach { listModel in
|
||||
songVideos.forEach { song in
|
||||
if listModel.song.videoId == song.videoId {
|
||||
listModel.index = song.index
|
||||
listModel.song.index = song.index
|
||||
if currentVideo != nil {
|
||||
//判断是否当前音乐
|
||||
if currentVideo.song.videoId == targetVideoId {
|
||||
//判断targetIndex是否大于最大音乐值
|
||||
if targetIndex < songVideos.count {
|
||||
let videoId = songVideos[targetIndex].videoId ?? ""
|
||||
improveData(videoId)
|
||||
}else {
|
||||
//移除的是原来最后一首音乐,播放新的最后一首音乐
|
||||
let videoId = songVideos.last?.videoId ?? ""
|
||||
improveData(videoId)
|
||||
}
|
||||
if currentVideo.song.videoId == song.videoId {
|
||||
currentVideo.index = song.index
|
||||
currentVideo.song.index = song.index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//判断是否当前音乐
|
||||
if currentVideo.song.videoId == targetVideoId {
|
||||
if let videoId = songVideos.first(where: {$0.index == targetIndex})?.videoId {
|
||||
//更新当前音乐
|
||||
improveData(videoId)
|
||||
}else {
|
||||
//移除的是原来最后一首音乐,播放新的最后一首音乐
|
||||
let videoId = songVideos.last?.videoId ?? ""
|
||||
improveData(videoId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
///b面tabBar控制器
|
||||
class MPPositive_TabBarController: UITabBarController {
|
||||
class MPPositive_TabBarController: UITabBarController, UIViewControllerTransitioningDelegate {
|
||||
//自定义tabBar
|
||||
private lazy var customTabBar:MPPositive_CustomTabBar = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: 72*width))
|
||||
private lazy var bottomView:MPPositive_BottomShowView = .init(frame: .init(x: 0, y: 0, width: 351, height: 82))
|
||||
@ -36,6 +36,17 @@ class MPPositive_TabBarController: UITabBarController {
|
||||
make.width.equalTo(351*width)
|
||||
make.height.equalTo(82*width)
|
||||
}
|
||||
bottomView.showListBlock = {
|
||||
[weak self] in
|
||||
if MP_PlayerManager.shared.loadPlayer != nil {
|
||||
MPPositive_ModalType = .PlayerList
|
||||
let listVC = MPPositive_PlayerListShowViewController()
|
||||
listVC.transitioningDelegate = self
|
||||
listVC.modalPresentationStyle = .custom
|
||||
self?.present(listVC, animated: true)
|
||||
}
|
||||
}
|
||||
bottomView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pupPlayerAction)))
|
||||
addNotification()
|
||||
}
|
||||
//监听通知
|
||||
@ -43,12 +54,19 @@ class MPPositive_TabBarController: UITabBarController {
|
||||
//监听标签切换
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(switchAction(_:)), notificationName: .switch_tabBarItem)
|
||||
//监听控制器弹出
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction(_:)), notificationName: .pup_player_vc)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(pupPlayerAction), notificationName: .pup_player_vc)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .pup_bottom_show)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(bottomAnimationAction(_:)), notificationName: .player_delete_list)
|
||||
}
|
||||
deinit {
|
||||
//移除所有监听
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
|
||||
return MPPositive_PresentationController(presentedViewController: presented, presenting: presenting)
|
||||
}
|
||||
//弹出音乐播放器
|
||||
|
||||
}
|
||||
//MARK: - 通知处理
|
||||
extension MPPositive_TabBarController {
|
||||
@ -58,12 +76,33 @@ extension MPPositive_TabBarController {
|
||||
selectedIndex = tag
|
||||
}
|
||||
//弹出player控制器
|
||||
@objc private func pupPlayerAction(_ sender:Notification) {
|
||||
//检索播放器中是否存在load模型
|
||||
if MP_PlayerManager.shared.loadPlayer != nil {
|
||||
let playerVC = MPPositive_PlayerViewController()
|
||||
playerVC.modalPresentationStyle = .fullScreen
|
||||
present(playerVC, animated: true)
|
||||
@objc private func pupPlayerAction() {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
//检索播放器中是否存在load模型
|
||||
if MP_PlayerManager.shared.loadPlayer != nil {
|
||||
let playerVC = MPPositive_PlayerViewController()
|
||||
playerVC.modalPresentationStyle = .fullScreen
|
||||
self?.present(playerVC, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
//切换底部音乐模块状态
|
||||
@objc private func bottomAnimationAction(_ sender:Notification) {
|
||||
switch_bottomShowAnimation(MP_PlayerManager.shared.loadPlayer != nil)
|
||||
}
|
||||
//底部BottomView的切换动画
|
||||
private func switch_bottomShowAnimation(_ state:Bool) {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
[weak self] in
|
||||
guard let self = self else { return }
|
||||
if state {
|
||||
//向上展示
|
||||
bottomView.transform = .init(translationX: 0, y: -145*width)
|
||||
}else {
|
||||
//向下隐藏
|
||||
bottomView.transform = .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,8 +31,12 @@ class MPPositive_PlayerListShowViewController: UIViewController {
|
||||
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
view.layer.cornerRadius = 18*width
|
||||
configure()
|
||||
//添加监听,当前音乐切换后,刷新
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(currentVideoReloadAction(_:)), notificationName: .positive_player_reload)
|
||||
}
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
view.addSubview(indictorImageView)
|
||||
indictorImageView.snp.makeConstraints { make in
|
||||
@ -51,6 +55,10 @@ class MPPositive_PlayerListShowViewController: UIViewController {
|
||||
super.viewWillAppear(animated)
|
||||
tableView.reloadData()
|
||||
}
|
||||
@objc private func currentVideoReloadAction(_ sender:Notification) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
//MARK: - tableView
|
||||
extension MPPositive_PlayerListShowViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
@ -63,9 +71,16 @@ extension MPPositive_PlayerListShowViewController: UITableViewDataSource, UITabl
|
||||
cell.song = MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row]
|
||||
cell.removeBlock = {
|
||||
[weak self] in
|
||||
//从列表中移除音乐
|
||||
MP_PlayerManager.shared.loadPlayer.removeData(MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row].videoId)
|
||||
tableView.reloadData()
|
||||
if MP_PlayerManager.shared.loadPlayer.songVideos.count > 1 {
|
||||
//从列表中移除音乐
|
||||
MP_PlayerManager.shared.loadPlayer.removeData(MP_PlayerManager.shared.loadPlayer.songVideos[indexPath.row].videoId)
|
||||
tableView.reloadData()
|
||||
}else {
|
||||
//当音乐库只有一首的话,移除的话,直接关闭播放器
|
||||
MP_PlayerManager.shared.stop()
|
||||
MP_PlayerManager.shared.loadPlayer = nil
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
@ -74,6 +74,8 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
btn.setBackgroundImage(UIImage(named: "Player_Pause'logo"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Player_Player'logo"), for: .selected)
|
||||
btn.addTarget(self, action: #selector(playClick(_ :)), for: .touchUpInside)
|
||||
//默认无法交互(以免用户交互导致网络请求混乱)
|
||||
btn.isUserInteractionEnabled = false
|
||||
return btn
|
||||
}()
|
||||
//歌单列表按钮
|
||||
@ -87,7 +89,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
private lazy var typeBtn:UIButton = {
|
||||
let btn = UIButton()
|
||||
btn.setBackgroundImage(UIImage(named: "List_NormolPlay'logo"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "List_ShufflePlay'logo"), for: .selected)
|
||||
btn.addTarget(self, action: #selector(typeClick(_ :)), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
@ -115,10 +116,43 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
configure()
|
||||
//添加监听
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(playerReloadAction(_ :)), notificationName: .positive_player_reload)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(statusSwitchAction(_:)), notificationName: .switch_player_status)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(playerTypeSwitchAction(_:)), notificationName: .player_type_switch)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(deleteListAction(_:)), notificationName: .player_delete_list)
|
||||
//打开时检索播放器状态,好调整内容
|
||||
MP_PlayerManager.shared.runActionBlock = { [weak self] (currentTime, duration) in
|
||||
guard let self = self else { return }
|
||||
//展示当前时间
|
||||
coverView.durationLabel.text = setTimesToMinSeconds(currentTime)
|
||||
//展示剩余时间
|
||||
let remain:TimeInterval = duration - currentTime
|
||||
coverView.maxTimesLabel.text = setTimesToMinSeconds(remain)
|
||||
//调整进度条内容
|
||||
let value = currentTime/duration
|
||||
coverView.sliderView.value = Float(value)
|
||||
}
|
||||
switch MP_PlayerManager.shared.getPlayState() {
|
||||
case .Null://说明播放器还未尝试过播放
|
||||
playBtn.isSelected = false
|
||||
playBtn.isUserInteractionEnabled = false
|
||||
case .Playing://播放中
|
||||
playBtn.isSelected = true
|
||||
playBtn.isUserInteractionEnabled = true
|
||||
default://暂停中
|
||||
playBtn.isSelected = false
|
||||
playBtn.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
//判断播放器是否装填数据
|
||||
if MP_PlayerManager.shared.loadPlayer.currentVideo != nil {
|
||||
uploadUI()
|
||||
}
|
||||
}
|
||||
//视图配置
|
||||
private func configure() {
|
||||
//导航View内容配置
|
||||
@ -251,39 +285,70 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
}
|
||||
return bottomView
|
||||
}
|
||||
//MARK: - 页面渲染
|
||||
private func uploadUI() {
|
||||
//填充数据
|
||||
backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl)
|
||||
coverView.coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl)
|
||||
coverView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title
|
||||
coverView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle
|
||||
lyricsView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title
|
||||
lyricsView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle
|
||||
lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics ?? "No Lyrics"
|
||||
}
|
||||
//MARK: - 通知
|
||||
//播放器音乐刷新
|
||||
@objc private func playerReloadAction(_ sender:Notification) {
|
||||
//渲染页面
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
backImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl)
|
||||
coverView.coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer.currentVideo?.coverUrl)
|
||||
coverView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title
|
||||
coverView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle
|
||||
lyricsView.titleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.title
|
||||
lyricsView.subtitleLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo?.subtitle
|
||||
lyricsView.lyricsLabel.text = MP_PlayerManager.shared.loadPlayer.currentVideo.lyrics ?? "No Lyrics"
|
||||
//启动播放
|
||||
uploadUI()
|
||||
//回正进度条
|
||||
coverView.sliderView.value = 0
|
||||
//调整时间值
|
||||
coverView.durationLabel.text = setTimesToMinSeconds(0)
|
||||
//调整最大时间值
|
||||
coverView.maxTimesLabel.text = setTimesToMinSeconds(0)
|
||||
//开始播放
|
||||
MP_PlayerManager.shared.play { [weak self] in
|
||||
guard let self = self else { return }
|
||||
//回正进度条
|
||||
coverView.sliderView.value = 0
|
||||
playBtn.isSelected = true
|
||||
//允许playBtn按钮交互
|
||||
playBtn.isUserInteractionEnabled = true
|
||||
} runAction: { [weak self] (currentTime, duration) in
|
||||
guard let self = self else { return }
|
||||
//展示当前时间
|
||||
coverView.durationLabel.text = setTimesToMinSeconds(currentTime)
|
||||
//展示剩余时间
|
||||
let remain:TimeInterval = duration - currentTime
|
||||
coverView.maxTimesLabel.text = setTimesToMinSeconds(remain)
|
||||
//调整进度条内容
|
||||
let value = currentTime/duration
|
||||
coverView.sliderView.value = Float(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
//切换播放器状态时
|
||||
@objc private func statusSwitchAction(_ sender:Notification) {
|
||||
if sender.object != nil {
|
||||
let state:MP_PlayerStateType = sender.object as! MP_PlayerStateType
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
switch state {
|
||||
case .Playing:
|
||||
self?.playBtn.isSelected = true
|
||||
default:
|
||||
self?.playBtn.isSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//切换播放器播放方式
|
||||
@objc private func playerTypeSwitchAction(_ sender:Notification) {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
switchPlayTypeBtnIcon(typeBtn)
|
||||
}
|
||||
}
|
||||
//用户清空了歌单列表
|
||||
@objc private func deleteListAction(_ sender:Notification) {
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
//MARK: - 点击事件
|
||||
//向下dismiss
|
||||
@objc private func disMissClick(_ sender:UIButton) {
|
||||
@ -343,6 +408,10 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
|
||||
//播放/暂停/继续
|
||||
@objc private func playClick(_ sender:UIButton) {
|
||||
guard MP_PlayerManager.shared.loadPlayer != nil else {
|
||||
return
|
||||
}
|
||||
//在当前音乐填充好之前,禁止触发点击
|
||||
switch MP_PlayerManager.shared.getPlayState() {
|
||||
case .Null:
|
||||
//启动播放
|
||||
@ -350,36 +419,23 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
guard let self = self else { return }
|
||||
//回正进度条
|
||||
coverView.sliderView.value = 0
|
||||
sender.isSelected = true
|
||||
} runAction: { [weak self] (currentTime, duration) in
|
||||
guard let self = self else { return }
|
||||
//展示当前时间
|
||||
coverView.durationLabel.text = setTimesToMinSeconds(currentTime)
|
||||
//展示剩余时间
|
||||
let remain:TimeInterval = duration - currentTime
|
||||
coverView.maxTimesLabel.text = setTimesToMinSeconds(remain)
|
||||
//调整进度条内容
|
||||
let value = currentTime/duration
|
||||
coverView.sliderView.value = Float(value)
|
||||
}
|
||||
|
||||
case .Playing:
|
||||
//播放中,进入暂停
|
||||
MP_PlayerManager.shared.pause {
|
||||
[weak self] in
|
||||
sender.isSelected = false
|
||||
}
|
||||
case .Pause:
|
||||
//暂停中,进入继续
|
||||
MP_PlayerManager.shared.resume {
|
||||
[weak self] in
|
||||
sender.isSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
//展示列表
|
||||
@objc private func listClick(_ sender:UIButton) {
|
||||
if MP_PlayerManager.shared.loadPlayer != nil {
|
||||
MPPositive_ModalType = .PlayerList
|
||||
let listVC = MPPositive_PlayerListShowViewController()
|
||||
listVC.transitioningDelegate = self
|
||||
listVC.modalPresentationStyle = .custom
|
||||
@ -391,12 +447,17 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
}
|
||||
//切换播放器状态(按顺序/随机/单曲)
|
||||
@objc private func typeClick(_ sender:UIButton) {
|
||||
|
||||
//对播放器播放方式截形切换
|
||||
var value = MP_PlayerManager.shared.getPlayType().rawValue
|
||||
value += 1
|
||||
if value > 2 {
|
||||
value = 0
|
||||
}
|
||||
MP_PlayerManager.shared.setPlayType(.init(rawValue: value)!)
|
||||
}
|
||||
//下一首
|
||||
@objc private func nextClick(_ sender:UIButton) {
|
||||
coverView.sliderView.value = 0
|
||||
playBtn.isSelected = false
|
||||
playBtn.isUserInteractionEnabled = false
|
||||
MP_PlayerManager.shared.nextEvent()
|
||||
|
||||
@ -404,7 +465,6 @@ class MPPositive_PlayerViewController: MPPositive_BaseViewController, UIViewCont
|
||||
//上一首
|
||||
@objc private func previousClick(_ sender:UIButton) {
|
||||
coverView.sliderView.value = 0
|
||||
playBtn.isSelected = false
|
||||
playBtn.isUserInteractionEnabled = false
|
||||
MP_PlayerManager.shared.previousEvent()
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
//
|
||||
// MPPositive_SearchResultShowViewController.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
///搜索结果控制器
|
||||
class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setTitle("Result")
|
||||
setPopBtn()
|
||||
}
|
||||
|
||||
}
|
||||
@ -7,23 +7,201 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class MPPositive_SearchViewController: UIViewController {
|
||||
class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
||||
//背景图片
|
||||
private lazy var bgImageView:UIImageView = {
|
||||
let imageView:UIImageView = .init(image: .init(named: "B_Home_BG'bg"))
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
//顶部搜索textField
|
||||
private lazy var searchTextField:UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.delegate = self
|
||||
textField.font = .systemFont(ofSize: 14*width, weight: .regular)
|
||||
textField.textColor = .white
|
||||
//设置一个富文本占位符
|
||||
let attributedText = NSAttributedString(string: "Search songs,artists,playlists", attributes: [.font:UIFont.systemFont(ofSize: 14*width, weight: .regular), .foregroundColor:UIColor(hex: "#666666")])
|
||||
textField.attributedPlaceholder = attributedText
|
||||
return textField
|
||||
}()
|
||||
//搜索建议tableView
|
||||
private lazy var suggestionsTableView:UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.backgroundColor = .init(hex: "#151718")
|
||||
tableView.separatorStyle = .none
|
||||
//设置一个边框
|
||||
tableView.layer.borderWidth = 1*width
|
||||
tableView.layer.borderColor = UIColor(hex: "#FFFFFF", alpha: 0.35).cgColor
|
||||
tableView.layer.masksToBounds = true
|
||||
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
tableView.layer.cornerRadius = 10*width
|
||||
|
||||
tableView.rowHeight = 60*width
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(MPPositive_SearchSuggestionItemTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionItemTableViewCellID)
|
||||
tableView.register(MPPositive_SearchSuggestionListTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionListTableViewCellID)
|
||||
return tableView
|
||||
}()
|
||||
private let MPPositive_SearchSuggestionItemTableViewCellID = "MPPositive_SearchSuggestionItemTableViewCell"
|
||||
private let MPPositive_SearchSuggestionListTableViewCellID = "MPPositive_SearchSuggestionListTableViewCell"
|
||||
//对用户展示的搜索建议组
|
||||
private var suggestions:[[MPPositive_SearchSuggestionItemModel]]!{
|
||||
willSet{
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
if newValue != nil {
|
||||
//搜索到了
|
||||
var count:Int = 0
|
||||
newValue.forEach { items in
|
||||
count += items.count
|
||||
}
|
||||
suggestionsTableView.isHidden = false
|
||||
suggestionsTableView.snp.updateConstraints { make in
|
||||
make.height.equalTo(CGFloat(count*60)*width)
|
||||
}
|
||||
}else {
|
||||
//没有搜索到
|
||||
suggestionsTableView.isHidden = true
|
||||
suggestionsTableView.snp.updateConstraints { make in
|
||||
make.height.equalTo(0)
|
||||
}
|
||||
}
|
||||
suggestionsTableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
//搜索限定计时器
|
||||
private var debounceTimer: Timer?
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
setTitle("")
|
||||
configure()
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
searchTextField.text = ""
|
||||
suggestionsTableView.isHidden = true
|
||||
suggestions = nil
|
||||
}
|
||||
//配置
|
||||
private func configure() {
|
||||
let searchView = createSearchView()
|
||||
navView.addSubview(searchView)
|
||||
searchView.snp.makeConstraints { make in
|
||||
make.width.equalTo(339*width)
|
||||
make.height.equalTo(32*width)
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
view.addSubview(bgImageView)
|
||||
bgImageView.snp.makeConstraints { make in
|
||||
make.top.right.left.equalToSuperview()
|
||||
make.height.equalTo(981*width)
|
||||
}
|
||||
view.addSubview(suggestionsTableView)
|
||||
suggestionsTableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(searchView.snp.bottom)
|
||||
make.left.equalTo(searchView.snp.left).offset(16*width)
|
||||
make.right.equalTo(searchView.snp.right).offset(-16*width)
|
||||
make.height.equalTo(240*width)
|
||||
}
|
||||
suggestionsTableView.isHidden = true
|
||||
}
|
||||
//生成一个顶部搜索框
|
||||
private func createSearchView() -> UIView{
|
||||
let searchView:UIView = UIView()
|
||||
searchView.backgroundColor = .init(hex: "#212121")
|
||||
searchView.isUserInteractionEnabled = true
|
||||
searchView.layer.masksToBounds = true
|
||||
searchView.layer.cornerRadius = 16*width
|
||||
//添加一个icon
|
||||
let iconImageView = UIImageView(image: .init(named: "B_Seach"))
|
||||
searchView.addSubview(iconImageView)
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.height.width.equalTo(16*width)
|
||||
make.left.equalToSuperview().offset(16*width)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
//添加textField
|
||||
searchView.addSubview(searchTextField)
|
||||
searchTextField.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.left.equalTo(iconImageView.snp.right).offset(8*width)
|
||||
make.right.equalToSuperview().offset(-16*width)
|
||||
}
|
||||
return searchView
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
//MARK: - textField
|
||||
extension MPPositive_SearchViewController:UITextFieldDelegate {
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
let text = (textField.text! as NSString).replacingCharacters(in: range, with: string)
|
||||
guard text.count <= 30 else {
|
||||
return false
|
||||
}
|
||||
if text.isEmpty {
|
||||
suggestions = nil
|
||||
}else {
|
||||
//触发网络请求
|
||||
loadSearchSuggestions(text)
|
||||
}
|
||||
return true
|
||||
}
|
||||
//避免重复请求
|
||||
private func cancelDebounceTimer() {
|
||||
debounceTimer?.invalidate()
|
||||
debounceTimer = nil
|
||||
}
|
||||
//当用户输入文本通过后进行检索
|
||||
private func loadSearchSuggestions(_ text:String) {
|
||||
cancelDebounceTimer()
|
||||
//停止输入0.8秒后调用
|
||||
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false) { [weak self] _ in
|
||||
self?.fetchSearchSuggestions(text)
|
||||
}
|
||||
}
|
||||
//获取建议组
|
||||
private func fetchSearchSuggestions(_ text:String) {
|
||||
MP_NetWorkManager.shared.requestSearchSuggestions(text) { [weak self] (result) in
|
||||
self?.suggestions = result
|
||||
}
|
||||
}
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
//判断textField是否存在文本
|
||||
if let text = textField.text, text.isEmpty != true {
|
||||
//用户输入了文本
|
||||
let showVC = MPPositive_SearchResultShowViewController()
|
||||
|
||||
navigationController?.pushViewController(showVC, animated: true)
|
||||
return true
|
||||
}else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
//MARK: - tableView
|
||||
extension MPPositive_SearchViewController:UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return suggestions != nil ? suggestions.count:0
|
||||
}
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return suggestions != nil ? suggestions[section].count:0
|
||||
}
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if suggestions[indexPath.section][indexPath.row].reviewUrls != nil {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionListTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionListTableViewCell
|
||||
cell.item = suggestions[indexPath.section][indexPath.row]
|
||||
return cell
|
||||
}else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionItemTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionItemTableViewCell
|
||||
cell.item = suggestions[indexPath.section][indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
///b面底部播放音乐展示View
|
||||
class MPPositive_BottomShowView: UIView {
|
||||
//绿色背景图片
|
||||
@ -37,14 +38,22 @@ class MPPositive_BottomShowView: UIView {
|
||||
btn.addTarget(self, action: #selector(switchStatuClick(_ :)), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
//展开音乐播放列表
|
||||
var showListBlock:(() -> Void)?
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
//添加一个监听
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(currentVideoSwitchAction(_:)), notificationName: .positive_player_reload)
|
||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(statusSwitchAction(_:)), notificationName: .switch_player_status)
|
||||
confirgue()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
//配置
|
||||
private func confirgue() {
|
||||
addSubview(bgGreenImageView)
|
||||
@ -77,13 +86,62 @@ class MPPositive_BottomShowView: UIView {
|
||||
make.right.equalTo(titleLabel)
|
||||
}
|
||||
}
|
||||
|
||||
//切换当前播放音乐时会触发
|
||||
@objc private func currentVideoSwitchAction(_ sender:Notification) {
|
||||
//更新显示的内容
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
guard let self = self else {return}
|
||||
if MP_PlayerManager.shared.loadPlayer?.currentVideo != nil {
|
||||
//存在当前播放音乐,更新显示
|
||||
coverImageView.kf.setImage(with: MP_PlayerManager.shared.loadPlayer?.currentVideo.coverUrl, placeholder: placeholderImage)
|
||||
titleLabel.text = MP_PlayerManager.shared.loadPlayer?.currentVideo?.title ?? ""
|
||||
subtitleLabel.text = MP_PlayerManager.shared.loadPlayer?.currentVideo.subtitle ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
//切换播放器状态时
|
||||
@objc private func statusSwitchAction(_ sender:Notification) {
|
||||
if sender.object != nil {
|
||||
let state:MP_PlayerStateType = sender.object as! MP_PlayerStateType
|
||||
DispatchQueue.main.async {
|
||||
[weak self] in
|
||||
switch state {
|
||||
case .Playing:
|
||||
self?.playStatuBtn.isSelected = true
|
||||
default:
|
||||
self?.playStatuBtn.isSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//展开当前音乐列表
|
||||
@objc private func expandListsClick(_ sender:UIButton) {
|
||||
|
||||
guard showListBlock != nil else {
|
||||
return
|
||||
}
|
||||
showListBlock!()
|
||||
}
|
||||
//切换播放音乐状态
|
||||
@objc private func switchStatuClick(_ sender:UIButton) {
|
||||
|
||||
guard MP_PlayerManager.shared.loadPlayer != nil else {
|
||||
return
|
||||
}
|
||||
//在当前音乐填充好之前,禁止触发点击
|
||||
switch MP_PlayerManager.shared.getPlayState() {
|
||||
case .Null:
|
||||
//启动播放
|
||||
MP_PlayerManager.shared.play()
|
||||
case .Playing:
|
||||
//播放中,进入暂停
|
||||
MP_PlayerManager.shared.pause {
|
||||
[weak self] in
|
||||
}
|
||||
case .Pause:
|
||||
//暂停中,进入继续
|
||||
MP_PlayerManager.shared.resume {
|
||||
[weak self] in
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,14 @@ class MPPositive_PlayerListShowTableViewCell: UITableViewCell {
|
||||
}()
|
||||
var song:MPPositive_SongItemModel!{
|
||||
didSet{
|
||||
//判断是否当前播放
|
||||
if song.videoId == MP_PlayerManager.shared.loadPlayer.currentVideo.song.videoId {
|
||||
titleLabel.textColor = .init(hex: "#80F988")
|
||||
subtitleLabel.textColor = .init(hex: "#80F988")
|
||||
}else {
|
||||
titleLabel.textColor = .white
|
||||
subtitleLabel.textColor = .init(hex: "#FFFFFF", alpha: 0.6)
|
||||
}
|
||||
coverImageView.kf.setImage(with: URL(string: song.reviewUrls?.first ?? ""), placeholder: placeholderImage)
|
||||
titleLabel.text = song.title
|
||||
subtitleLabel.text = song.shortBylineText
|
||||
|
||||
@ -21,8 +21,8 @@ class MPPositive_PlayerLyricView: UIView {
|
||||
scrollView.addSubview(lyricsLabel)
|
||||
scrollView.contentSize = .init(width: screen_Width, height: lyricsLabel.frame.origin.y + lyricsLabel.frame.height)
|
||||
lyricsLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(20*width)
|
||||
make.right.equalToSuperview().offset(-20*width)
|
||||
make.width.equalToSuperview().multipliedBy(0.89)
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(15*width)
|
||||
make.bottom.equalToSuperview().offset(30*width)
|
||||
}
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
//
|
||||
// MPPositive_SearchSuggestionItemTableViewCell.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class MPPositive_SearchSuggestionItemTableViewCell: UITableViewCell {
|
||||
//搜索Icon
|
||||
private lazy var iconImageView:UIImageView = UIImageView(image: .init(named: "B_Seach"))
|
||||
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left)
|
||||
var item:MPPositive_SearchSuggestionItemModel!{
|
||||
didSet{
|
||||
titleLabel.text = item.title ?? ""
|
||||
}
|
||||
}
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
private func configure() {
|
||||
contentView.addSubview(iconImageView)
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(12*width)
|
||||
make.width.height.equalTo(30*width)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
contentView.addSubview(titleLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalTo(iconImageView.snp.right).offset(10*width)
|
||||
make.right.equalToSuperview().offset(-12*width)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
//
|
||||
// MPPositive_SearchSuggestionListTableViewCell.swift
|
||||
// MusicPlayer
|
||||
//
|
||||
// Created by Mr.Zhou on 2024/5/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
class MPPositive_SearchSuggestionListTableViewCell: UITableViewCell {
|
||||
//搜索Icon
|
||||
private lazy var reviewImageView:UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .white, textAlignment: .left)
|
||||
private lazy var subtitleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left)
|
||||
var item:MPPositive_SearchSuggestionItemModel!{
|
||||
didSet{
|
||||
reviewImageView.kf.setImage(with: URL(string: item.reviewUrls?.last ?? ""), placeholder: placeholderImage)
|
||||
titleLabel.text = item.title ?? ""
|
||||
subtitleLabel.text = item.subtitle ?? ""
|
||||
}
|
||||
}
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
configure()
|
||||
}
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
private func configure() {
|
||||
contentView.addSubview(reviewImageView)
|
||||
reviewImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(12*width)
|
||||
make.width.height.equalTo(30*width)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
contentView.addSubview(titleLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(reviewImageView.snp.top)
|
||||
make.left.equalTo(reviewImageView.snp.right).offset(10*width)
|
||||
make.right.equalToSuperview().offset(-12*width)
|
||||
}
|
||||
contentView.addSubview(subtitleLabel)
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(reviewImageView.snp.bottom)
|
||||
make.left.equalTo(titleLabel.snp.left)
|
||||
make.right.equalTo(titleLabel.snp.right)
|
||||
}
|
||||
}
|
||||
}
|
||||