<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      OC高仿iOS網(wǎng)易云音樂AFNetworking+SDWebImage+MJRefresh+MVC+MVVM

      效果

      i11.png

      因為OC版本大部分截圖和Swift版本一樣,所以就不再另外截圖了。

      列文章目錄

      因為目錄比較多,每次更新這里比較麻煩,所以推薦點(diǎn)擊到主頁,然后查看iOS云音樂專欄。

      目簡介

      這是一個使用OC語言(還有Swift,Android版本),從0開發(fā)一個iOS平臺,接近企業(yè)級的項目(我的云音樂),包含了基礎(chǔ)內(nèi)容,高級內(nèi)容,項目封裝,項目重構(gòu)等知識;主要是使用系統(tǒng)功能,流行的第三方框架,第三方服務(wù),完成接近企業(yè)級商業(yè)級項目。

      目功能點(diǎn)

      隱私協(xié)議對話框
      啟動界面和動態(tài)處理權(quán)限
      引導(dǎo)界面和廣告
      輪播圖和側(cè)滑菜單
      首頁復(fù)雜列表和列表排序
      音樂播放和音樂列表管理
      全局音樂控制條
      桌面歌詞和自定義樣式
      全局媒體控制中心
      評論和回復(fù)評論
      評論富文本點(diǎn)擊
      評論提醒人和話題
      朋友圈動態(tài)列表和發(fā)布
      高德地圖定位和路徑規(guī)劃
      阿里云OSS上傳
      視頻播放和控制
      QQ/微信登錄和分享
      商城/購物車\微信\支付寶支付
      文本和圖片聊天
      消息離線推送
      自動和手動檢查更新
      內(nèi)存泄漏和優(yōu)化
      ...

      發(fā)環(huán)境概述

      2022年5月開發(fā)完成的,所以全部都是最新的,平均每3年會重新制作,現(xiàn)在已經(jīng)是第三版了。

      Xcode 13.4
      iOS 15
      

      譯和運(yùn)行

      先安裝pod,用最新Xcode打開MyCloudMusic.xcworkspace,然后運(yùn)行,如果要運(yùn)行到真機(jī),先登陸自己的開發(fā)者賬戶,如果不是付費(fèi)賬戶,請刪除推送等付費(fèi)功能,更改BundleId,然后運(yùn)行。

      目目錄結(jié)構(gòu)

      ├── MyCloudMusic
      │   ├── AppDelegate.h
      │   ├── AppDelegate.m
      │   ├── Assets.xcassets #資源目錄
      │   ├── Base.lproj
      │   ├── Cell #通用cell
      │   ├── Component #每個功能模塊
      │   │   ├── Ad #廣告相關(guān)
      │   │   ├── Address #收貨地址相關(guān)
      │   ├── Config #配置目錄,例如:網(wǎng)絡(luò)地址配置
      │   ├── Controller #通用控制器
      │   ├── Extension #擴(kuò)展,例如:字符串?dāng)U展
      │   ├── Info.plist
      │   ├── Manager #管理器,例如:音樂播放管理器
      │   ├── Model  #通用模型
      │   ├── MyCloudMusic.entitlements
      │   ├── Network
      │   ├── PrefixHeader.pch
      │   ├── Repository #數(shù)據(jù)倉庫,例如:網(wǎng)絡(luò)請求封裝
      │   ├── Util #工具類
      │   ├── Vender #通過源碼方式依賴的第三方框架
      │   ├── View #通用View
      │   ├── ViewController.h
      │   ├── ViewController.m
      │   ├── main.m
      │   └── zh-Hans.lproj
      ├── MyCloudMusic.xcodeproj
      ├── MyCloudMusic.xcworkspace
      ├── MyCloudMusicTests
      │   └── MyCloudMusicTests.m
      ├── MyCloudMusicUITests
      ├── Podfile
      ├── Podfile.lock
      ├── R.h
      ├── R.m
      └── ixueaeduTestVideo.mp4
      

      賴框架

      內(nèi)容太多,只列出部分。

      target 'MyCloudMusic' do
        # Comment the next line if you don't want to use dynamic frameworks
        use_frameworks!
      
        # Pods for MyCloudMusic
        #騰訊開源的UI框架,提供了很多功能,例如:圓角按鈕,空心按鈕,TextView支持placeholder
        #https://github.com/QMUI/QMUIDemo_iOS
        #https://qmuiteam.com/ios/get-started
        pod "QMUIKit"
        
        #https://github.com/SysdataSpA/R.objc
        #作者說受R.swift的自由啟發(fā),獲取自動完成的本地化字符串、資產(chǎn)目錄圖像名稱和故事板對象
        pod 'R.objc'
        
        #輪播圖
        #https://github.com/QuintGao/GKCycleScrollView
        pod 'GKCycleScrollView'
        
        #網(wǎng)絡(luò)框架
        #https://github.com/AFNetworking/AFNetworking
        pod 'AFNetworking'
      
        
        #輪播圖,多講解一個是方便大家選擇
        #https://github.com/wwmz/WMZBanner
        pod 'WMZBanner'
        
        #https://github.com/91renb/BRPickerView
        #封裝的是iOS中常用的選擇器組件,主要包括:日期選擇器
        pod 'BRPickerView'
        
        #支付寶支付
        #https://docs.open.alipay.com/204/105295/
        pod 'AlipaySDK-iOS'
        
        #融云聊天
        #https://doc.rongcloud.cn/im/IOS/5.X/noui/import
        pod 'RongCloudIM/IMLib'
        
        pod 'JCore'
      
        #極光推送
        #https://docs.jiguang.cn/jpush/client/iOS/ios_guide_new/
        pod 'JPush'
        
        #極光統(tǒng)計
        #https://docs.jiguang.cn/janalytics/guideline/intro/
        pod 'JAnalytics'
        
        #webview和js交互框架
        #可以直接使用系統(tǒng)提供的api,不是說一定要用框架
        #只是用該框架,更方便
        #https://github.com/marcuswestin/WebViewJavascriptBridge
        pod 'WebViewJavascriptBridge'
        
        target 'MyCloudMusicTests' do
          inherit! :search_paths
          # Pods for testing
        end
      
        target 'MyCloudMusicUITests' do
          # Pods for testing
        end
      
      end
      

      戶協(xié)議對話框

      使用自定義Dialog實(shí)現(xiàn)。

      @interface TermServiceDialogController ()<QMUIModalPresentationContentViewControllerProtocol>
      
      @end
      
      @implementation TermServiceDialogController
      - (void)initViews{
          [super initViews];
          
          self.view.backgroundColor=[UIColor colorDivider];
          self.view.myWidth=MyLayoutSize.fill;
          self.view.myHeight=MyLayoutSize.wrap;
          
          //根容器
          self.rootContainer = [[MyLinearLayout alloc] initWithOrientation:MyOrientation_Vert];
          self.rootContainer.subviewSpace=0.5;
          self.rootContainer.myWidth=MyLayoutSize.fill;
          self.rootContainer.myHeight=MyLayoutSize.wrap;
          [self.view addSubview:self.rootContainer];
          
          //內(nèi)容容器
          self.contentContainer = [[MyLinearLayout alloc] initWithOrientation:MyOrientation_Vert];
          self.contentContainer.subviewSpace=25;
          self.contentContainer.myWidth=MyLayoutSize.fill;
          self.contentContainer.myHeight=MyLayoutSize.wrap;
          self.contentContainer.backgroundColor = [UIColor colorBackground];
          self.contentContainer.padding=UIEdgeInsetsMake(PADDING_LARGE2, PADDING_OUTER, PADDING_LARGE2, PADDING_OUTER);
          self.contentContainer.gravity=MyGravity_Horz_Center;
          [self.rootContainer addSubview:self.contentContainer];
          
          //標(biāo)題
          [self.contentContainer addSubview:self.titleView];
          
          self.textView=[UITextView new];
          self.textView.myWidth=MyLayoutSize.fill;
          
          //超出的內(nèi)容,自動支持滾動
          self.textView.myHeight=230;
          self.textView.text=@"...";
          self.textView.backgroundColor = [UIColor clearColor];
          
          //禁用編輯
          self.textView.editable=NO;
          
          [self.contentContainer addSubview:self.textView];
          
          [self.contentContainer addSubview:self.primaryButton];
          
          //不同意按鈕按鈕
          self.disagreeButton = [ViewFactoryUtil linkButton];
          [self.disagreeButton setTitle:R.string.localizable.disagree forState: UIControlStateNormal];
          [self.disagreeButton setTitleColor:[UIColor black80] forState:UIControlStateNormal];
          [self.disagreeButton addTarget:self action:@selector(disagreeClick:) forControlEvents:UIControlEventTouchUpInside];
          [self.disagreeButton sizeToFit];
          [self.contentContainer addSubview:self.disagreeButton];
      }
      
      - (void)show{
          self.modalController = [QMUIModalPresentationViewController new];
          self.modalController.animationStyle = QMUIModalPresentationAnimationStyleFade;
          
          //點(diǎn)擊外部不隱藏
          [self.modalController setModal:YES];
          
          //邊距
          self.modalController.contentViewMargins=UIEdgeInsetsMake(PADDING_LARGE2, PADDING_LARGE2, PADDING_LARGE2, PADDING_LARGE2);
          
          //設(shè)置要顯示的內(nèi)容控件
          self.modalController.contentViewController=self;
          
          [self.modalController showWithAnimated:YES completion:nil];
      }
      
      - (void)hide{
          [self.modalController hideWithAnimated:YES completion:nil];
      }
      
      #pragma mark - 創(chuàng)建控件
      - (UILabel *)titleView{
          if (!_titleView) {
              _titleView=[UILabel new];
              _titleView.myWidth=MyLayoutSize.fill;
              _titleView.myHeight=MyLayoutSize.wrap;
              _titleView.text=@"標(biāo)題";
              _titleView.textAlignment=NSTextAlignmentCenter;
              _titleView.font=[UIFont boldSystemFontOfSize:TEXT_LARGE3];
              _titleView.textColor=[UIColor colorOnSurface];
          }
          return _titleView;
      }
      
      - (QMUIButton *)primaryButton{
          if (!_primaryButton) {
              _primaryButton = [ViewFactoryUtil primaryHalfFilletButton];
              [_primaryButton setTitle:R.string.localizable.agree forState:UIControlStateNormal];
          }
          return _primaryButton;
      }
      @end
      

      導(dǎo)界面

      i9.png

      引導(dǎo)界面比較簡單,就是多個圖片可以左右滾動。

      @interface GuideController ()<GKCycleScrollViewDataSource,GKCycleScrollViewDelegate>
      @property (nonatomic, strong) GKCycleScrollView *contentScrollView;
      @end
      
      @implementation GuideController
      - (void)initViews{
          [super initViews];
          
          [self initLinearLayoutSafeArea];
          
          //輪播圖器容器
          MyRelativeLayout *bannerContainer=[MyRelativeLayout new];
          bannerContainer.myWidth=MyLayoutSize.fill;
          bannerContainer.myHeight=MyLayoutSize.wrap;
          bannerContainer.weight=1;
          [self.container addSubview:bannerContainer];
          
          //輪播圖
          _contentScrollView=[GKCycleScrollView new];
          _contentScrollView.backgroundColor = [UIColor clearColor];
          _contentScrollView.dataSource = self;
          _contentScrollView.delegate = self;
          _contentScrollView.myWidth = MyLayoutSize.fill;
          _contentScrollView.myHeight = MyLayoutSize.fill;
          
          //禁用自動滾動
          _contentScrollView.isAutoScroll=NO;
          
          //不改變透明度
          _contentScrollView.isChangeAlpha=NO;
          
          _contentScrollView.clipsToBounds = YES;
          [bannerContainer addSubview:_contentScrollView];
          
          //按鈕容器
          MyLinearLayout *controlContainer=[[MyLinearLayout alloc] initWithOrientation:MyOrientation_Horz];
          controlContainer.myBottom=PADDING_LARGE2;
          controlContainer.myWidth=MyLayoutSize.fill;
          controlContainer.myHeight=MyLayoutSize.wrap;
          
          //水平拉升,左,中,右間距一樣
          controlContainer.gravity = MyGravity_Horz_Among;
          [self.container addSubview:controlContainer];
          
          //登錄注冊按鈕
          QMUIButton *primaryButton = [ViewFactoryUtil primaryButton];
          [primaryButton setTitle:R.string.localizable.loginOrRegister forState:UIControlStateNormal];
          [primaryButton addTarget:self action:@selector(onPrimaryClick:) forControlEvents:UIControlEventTouchUpInside];
          primaryButton.myWidth=BUTTON_WIDTH_MEDDLE;
          [controlContainer addSubview:primaryButton];
      }
      
      - (void)initDatum{
          [super initDatum];
          self.datum = [NSMutableArray array];
          
          [self.datum addObject:R.image.guide1];
          [self.datum addObject:R.image.guide2];
          [self.datum addObject:R.image.guide3];
          [self.datum addObject:R.image.guide4];
          [self.datum addObject:R.image.guide5];
          [_contentScrollView reloadData];
      }
      
      - (void)onPrimaryClick:(QMUIButton *)sender{
          [AppDelegate.shared toLogin];
      }
      
      
      #pragma mark  輪播圖數(shù)據(jù)源
      
      /// 有多少個
      /// @param cycleScrollView <#cycleScrollView description#>
      - (NSInteger)numberOfCellsInCycleScrollView:(GKCycleScrollView *)cycleScrollView{
          return self.datum.count;
      }
      
      /// 返回cell
      /// @param cycleScrollView <#cycleScrollView description#>
      /// @param index <#index description#>
      - (GKCycleScrollViewCell *)cycleScrollView:(GKCycleScrollView *)cycleScrollView cellForViewAtIndex:(NSInteger)index {
          GKCycleScrollViewCell *cell = [cycleScrollView dequeueReusableCell];
          if (!cell) {
              cell = [GKCycleScrollViewCell new];
          }
      
          UIImage *data=[self.datum objectAtIndex:index];
      
          cell.imageView.image = data;
          cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
      
          return cell;
      }
      @end
      

      廣告界面

      i10.png

      實(shí)現(xiàn)圖片廣告和視頻廣告,廣告數(shù)據(jù)是在首頁是緩存到本地,目的是在啟動界面加載更快,因為真實(shí)項目中,大部分項目啟動頁面廣告時間一共就5秒,如果太長了用戶體驗不好,如果是從網(wǎng)絡(luò)請求,那么網(wǎng)絡(luò)可能就耗時2秒左右,所以導(dǎo)致就美喲多少時間顯示廣告了。

      廣告

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

      廣告

      -(void)showVideoAd:(NSURL *)data{
          //播放應(yīng)用內(nèi)嵌入視頻,放根目錄中
          //同樣其他的文件,也可以通過這種方式讀取
      	//data = [NSBundle.mainBundle URLForResource:@"ixueaeduTestVideo" withExtension:@".mp4"];
      
          _player = [AVPlayer playerWithURL:data];
      
          //靜音
          _player.muted = YES;
      
          /// 添加進(jìn)度監(jiān)聽
          __weak typeof(self) weakSelf = self;
          [_player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
              //當(dāng)前時間,秒
              Float64 current=CMTimeGetSeconds(weakSelf.player.currentItem.currentTime);
      
              //總時間
              CGFloat duration =  CMTimeGetSeconds(weakSelf.player.currentItem.duration);
      
              if (current==duration) {
                  //視頻播放結(jié)束
                  [weakSelf next];
              } else {
                  [weakSelf.skipView setTitle:[R.string.localizable skipAdCount:(NSInteger)(duration-current)] forState:UIControlStateNormal];
                  weakSelf.skipView.myWidth=MyLayoutSize.wrap;
                  [weakSelf.skipView setNeedsLayout];
      
              }
          }];
      
          [self.player play];
      
          //顯示圖像
          self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
      
          //從中心等比縮放,完全顯示控件
          self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
      
          [self.view.layer insertSublayer:self.playerLayer atIndex:0];
      }
      

      顯示圖片就是顯示本地圖片了,沒什么難點(diǎn),就不貼代碼了。

      首頁/歌單詳情/黑膠唱片界面

      i11.png

      首頁沒有頂部是輪播圖,然后是可以左右的菜單,接下來是熱門歌單,推薦單曲,最后是首頁排序模塊;整體上使用RecycerView實(shí)現(xiàn),輪播圖:

      //輪播圖
      BannerCell *cell = [tableView dequeueReusableCellWithIdentifier:BannerCellName forIndexPath:indexPath];
      
      //綁定數(shù)據(jù)
      [cell bind:data];
      
      return cell;
      

      詳情

      頂部是歌單信息,通過Cell實(shí)現(xiàn),底部是列表,顯示歌單內(nèi)容的音樂,點(diǎn)擊音樂進(jìn)入黑膠唱片播放界面。

      @implementation SheetDetailController
      
      - (void)initViews{
          [super initViews];
          //添加背景圖片控件
          _backgroundImageView = [UIImageView new];
      
          //默認(rèn)隱藏
          _backgroundImageView.clipsToBounds = YES;
          _backgroundImageView.alpha = 0;
          _backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
          [self.view addSubview:self.backgroundImageView];
      
          ...
          
          //注冊歌單信息
          [self.tableView registerClass:[SheetInfoCell class] forCellReuseIdentifier:SheetInfoCellName];
          
          //注冊section
          [self.tableView registerClass:[SongGroupHeaderView class] forHeaderFooterViewReuseIdentifier:SongGroupHeaderViewName];
      
          //注冊單曲
          [self.tableView registerClass:[SongCell class] forCellReuseIdentifier:SongCellName];
      }
      
      - (void)initListeners{
          [super initListeners];
          @weakify(self);
          
          //點(diǎn)擊事件
          [QTSubMain(self,ClickEvent) next:^(ClickEvent *event) {
              @strongify(self);
              [self processClick:event.style];
          }];
      }
      
      ...
      
      -(void)loadData:(BOOL)isPlaceholder{
          [[DefaultRepository shared] sheetDetailWithId:_id success:^(BaseResponse * _Nonnull baseResponse, id  _Nonnull data) {
              [self show:data];
          }];
      }
      
      -(void)show:(Sheet *)data{
          self.data=data;
          
          [ImageUtil show:self.backgroundImageView uri:data.icon];
      
          //使用動畫顯示背景圖片
          [UIView animateWithDuration:0.3 animations:^{
              //透明度設(shè)置為1
              self.backgroundImageView.alpha=1;
          }];
          
          [self.datum removeAllObjects];
          
          //第一組
          SongGroupData *groupData=[SongGroupData new];
          NSMutableArray *tempArray = [NSMutableArray new];
          [tempArray addObject:data];
          groupData.datum=tempArray;
          [self.datum addObject:groupData];
          
          if (data.songs) {
              //有音樂才設(shè)置
      
              //設(shè)置數(shù)據(jù)
              groupData=[SongGroupData new];
              NSMutableArray *tempArray = [NSMutableArray new];
              [tempArray addObjectsFromArray:data.songs];
              [tempArray addObjectsFromArray:data.songs];
              groupData.datum=tempArray;
              [self.datum addObject:groupData];
          }
          
          [self.tableView reloadData];
      }
      
      /// 播放音樂
      /// @param data <#data description#>
      -(void)play:(Song *)data{
          //把當(dāng)前歌單所有音樂設(shè)置到播放列表
          //有些應(yīng)用
          //可能會實(shí)現(xiàn)添加到已經(jīng)播放列表功能
          [[MusicListManager shared] setDatum:self.data.songs];
      
          //播放當(dāng)前音樂
          [[MusicListManager shared] play:data];
          
          [self startMusicPlayerController];
      }
      
      /// 有多少組
      /// @param tableView <#tableView description#>
      - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
          return self.datum.count;
      }
      
      /// 當(dāng)前組有多少個
      /// @param tableView <#tableView description#>
      /// @param section <#section description#>
      - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
          SongGroupData *groupData=self.datum[section];
          return groupData.datum.count;
      }
      
      /// 返回section view
      /// @param tableView <#tableView description#>
      /// @param section <#section description#>
      - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
          __weak __typeof(self)weakSelf = self;
          
          //取出組數(shù)據(jù)
          SongGroupData *groupData=self.datum[section];
          
          //獲取header
          SongGroupHeaderView *header=[tableView dequeueReusableHeaderFooterViewWithIdentifier:   SongGroupHeaderViewName];
          
          [header setPlayAllClickBlock:^{
              __strong __typeof(weakSelf)strongSelf = weakSelf;
              
              if (strongSelf.datum.count>0) {
                  return;
              }
              
              SongGroupData *groupData=strongSelf.datum[1];
              Song *data= groupData.datum[0];
              
              [strongSelf play:data];
          }];
      
          //綁定數(shù)據(jù)
          [header bind:groupData];
      
          //返回header
          return header;
      }
      
      /// 返回當(dāng)前位置的cell
      /// 相當(dāng)于Android中RecyclerView Adapter的onCreateViewHolder
      /// @param tableView <#tableView description#>
      /// @param indexPath <#indexPath description#>
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
          SongGroupData *groupData=self.datum[indexPath.section];
          NSObject *data= groupData.datum[indexPath.row];
      
          //獲取類型
          ListStyle style=[self typeForItemAtData:data];
      
          switch (style) {
              case StyleSheet:{
                  //歌單
                  SheetInfoCell *cell = [tableView dequeueReusableCellWithIdentifier:SheetInfoCellName forIndexPath:indexPath];
                  
                  [cell bind:data];
                  
                  return cell;
              }
              ...
          }
      
      }
      
      /// Cell類型
      - (ListStyle)typeForItemAtData:(NSObject *)data{
              
          if([data isKindOfClass:[Sheet class]]){
              //歌單信息
              return StyleSheet;
          }
          
          return StyleSong;
      }
      
      /// header高度
      /// @param tableView <#tableView description#>
      /// @param section <#section description#>
      - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
          if (section==1) {
              return 50;
          }
          
          //其他組不顯示section
          return 0;
      }
      @end
      

      唱片

      上面是黑膠唱片,和網(wǎng)易云音樂差不多,隨著音樂滾動或暫停,頂部是控制相關(guān),音樂播放邏輯是封裝到MusicPlayerManager中:

      @implementation MusicPlayerManager
      
      /// 獲取單例對象
      +(instancetype)shared{
          static MusicPlayerManager *sharedInstance = nil;
          if (!sharedInstance) {
              sharedInstance = [[self alloc] init];
          }
          return sharedInstance;
          
      }
      
      - (instancetype)init{
          if (self=[super init]) {
              self.player = [[AVPlayer alloc] init];
              
              //默認(rèn)狀態(tài)
              self.status = PlayStatusNone;
          }
          return self;
      }
      
      - (void)play:(NSString *)uri data:(Song *)data{
          //設(shè)置音頻會話
          [SuperAudioSessionManager requestAudioFocus];
          
          //更改播放狀態(tài)
          _status = PlayStatusPlaying;
          
          //保存音樂對象
          self.data = data;
          
          NSURL *url=nil;
          if ([uri hasPrefix:@"http"]) {
              //網(wǎng)絡(luò)地址
              url=[NSURL URLWithString:uri];
          } else {
              //本地地址
              url=[NSURL fileURLWithPath:uri];
          }
          
          //創(chuàng)建一個播放Item
          AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
          
          //替換掉原來的播放Item
          [self.player replaceCurrentItemWithPlayerItem:item];
          
          //播放
          [self.player play];
          
          ...
      }
      
      -(void)prepareLyric{
          //歌詞處理
          //真實(shí)項目可能會
          //將歌詞這個部分拆分到其他組件中
          if (_data.parsedLyric) {
              //解析好了
              [self onLyricReady];
          } else if(_data.lyric) {
              //有歌詞,但是沒有解析
              [self parseLyric];
          }else{
              //沒有歌詞,并且不是本地音樂才請求
      
              //真實(shí)項目中可以會緩存歌詞
              //獲取歌詞數(shù)據(jù)
              [[DefaultRepository shared] songDetailWithId:_data.id success:^(BaseResponse * _Nonnull baseResponse, id  _Nonnull d) {
                  //請求成功
                  Song *data=d;
                  self.data.style=data.style;
                  self.data.lyric=data.lyric;
                  
                  [self parseLyric];
              }];
          }
      }
      
      -(void)parseLyric{
          if ([StringUtil isNotBlank:self.data.lyric]) {
              //有歌詞
              
              //在這里解析的好處是
              //外面不用管,直接使用
              self.data.parsedLyric = [LyricParser parse:self.data.style data:self.data.lyric];
          }
          
          //通知歌詞準(zhǔn)備好了
          [self onLyricReady];
      }
      
      -(void)onLyricReady{
          if (self.delegate) {
              [self.delegate onLyricReady:_data];
          }
      }
      
      -(void)initListeners{
          //KVO方式監(jiān)聽播放狀態(tài)
          //KVC:Key-Value Coding,另一種獲取對象字段的值,類似字典
          //KVO:Key-Value Observing,建立在KVC基礎(chǔ)上,能夠觀察一個字段值的改變
          [self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
          
          //監(jiān)聽音樂緩沖狀態(tài)
          [self.player.currentItem addObserver:self
                                    forKeyPath:@"loadedTimeRanges"  options:NSKeyValueObservingOptionNew
                                       context:nil];
          
          //播放結(jié)束事件
          [[NSNotificationCenter defaultCenter] addObserver:self
                                                   selector:@selector(onComplete:)
                                                       name:AVPlayerItemDidPlayToEndTimeNotification
                                                     object:self.player.currentItem];
      }
      
      /// 播放完畢了回調(diào)
      - (void)onComplete:(NSNotification *)notification {
          self.complete(_data);
      }
      
      /// 移除監(jiān)聽器
      -(void)removeListeners{
          [self.player.currentItem removeObserver:self forKeyPath:@"status" context:nil];
          [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges" context:nil];
          
      //    [[NSNotificationCenter defaultCenter] removeObserver:self];
      }
      
      
      /// KVO監(jiān)聽回調(diào)方法
      /// @param keyPath <#keyPath description#>
      /// @param object <#object description#>
      /// @param change <#change description#>
      /// @param context <#context description#>
      -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
      {
          //判斷監(jiān)聽的字段
          if ([keyPath isEqualToString:@"status"]) {
              switch (self.player.status) {
                      case AVPlayerStatusReadyToPlay:
                  {
                      //準(zhǔn)備播放完成了
                      //音樂的總時間
                      self.data.duration= CMTimeGetSeconds(self.player.currentItem.asset.duration);
                      
                      LogDebugTag(MusicPlayerManagerTag, @"observeValue status ReadyToPlay duration:%f",self.data.duration);
                                      
                      //回調(diào)代理
                      if (self.delegate) {
                          [self.delegate onPrepared:_data];
                      }
                      
                      //更新媒體控制中心信息
                      [self updateMediaInfo];
                      
                  }
                      break;
                      case AVPlayerStatusFailed:
                  {
                      //播放失敗了
                      _status = PlayStatusError;
                      
                      LogDebugTag(MusicPlayerManagerTag, @"observeValue status play error");
                  }
                      break;
                      
                  default:{
                      //未知狀態(tài)
                      LogDebugTag(MusicPlayerManagerTag, @"observeValue status unknown");
                      _status = PlayStatusNone;
                  }
                      break;
              }
              
          }
          ...
      }
      
      
      - (void)startPublishProgress{
          //判斷是否啟動了
          if (_playTimeObserve) {
              //已經(jīng)啟動了
              return;
          }
          
          @weakify(self);
                      
          //1/60秒,就是16毫秒
          self.playTimeObserve=[self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 60) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
              @strongify(self);
              
              //當(dāng)前播放的時間
              self.data.progress = CMTimeGetSeconds(time);
              
              //判斷是否有代理
              if (!self.delegate) {
                  //沒有回調(diào)
                  //停止定時器
                  [self stopPublishProgress];
                  return;
              }
              
              //回調(diào)代理
              [self.delegate onProgress:self.data];
              
              ...
      }
      
      - (void)stopPublishProgress{
          if (self.playTimeObserve) {
              [self.player removeTimeObserver:self.playTimeObserve];
              self.playTimeObserve=nil;
          }
          
      }
      
      - (BOOL)isPlaying{
          return _status == PlayStatusPlaying;
      }
      
      - (void)pause{
          //更改狀態(tài)
          _status = PlayStatusPause;
          
          //暫停
          [self.player pause];
          
          //移除監(jiān)聽器
          [self removeListeners];
      
          //回調(diào)代理
          if (self.delegate) {
              [self.delegate onPaused:_data];
          }
      
          //停止進(jìn)度分發(fā)定時器
          [self stopPublishProgress];
      }
      
      - (void)resume{
          //設(shè)置音頻會話
          [SuperAudioSessionManager requestAudioFocus];
          
          //更改播放狀態(tài)
          _status = PlayStatusPlaying;
          
          //播放
          [self.player play];
          
          ...
      }
      
      - (void)seekTo:(float)data{
          [self.player seekToTime:CMTimeMake(data, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
      }
      
      #pragma mark - 媒體中心
      
      /// 更新系統(tǒng)媒體控制中心信息
      /// 不需要更新進(jìn)度到控制中心
      /// 他那邊會自動倒計時
      /// 這部分可以重構(gòu)到公共類,因為像播放視頻也可以更新到系統(tǒng)媒體中心
      -(void)updateMediaInfo{
          //下載圖片,這部分應(yīng)該封裝,因為其他界面也用到了
          SDWebImageManager *manager =[SDWebImageManager sharedManager];
      
          NSURL *url= [NSURL URLWithString:[ResourceUtil resourceUri:self.data.icon]];
      
          [manager loadImageWithURL:url options:SDWebImageProgressiveLoad progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
              //進(jìn)度,這里用不到
          } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
              NSLog(@"load song image success");
              if (image!=NULL) {
                  [self setMediaInfo:image];
              }
          }];
      }
      
      - (void)setMediaInfo:(UIImage *)image{
          //初始化一個可變字典
          NSMutableDictionary *songInfo=[[NSMutableDictionary alloc] init];
      
          //初始化一個封面
          MPMediaItemArtwork *albumArt=[[MPMediaItemArtwork alloc] initWithBoundsSize:image.size requestHandler:^UIImage * _Nonnull(CGSize size) {
              return image;
          }];
      
          //設(shè)置封面
          [songInfo setObject:albumArt forKey:MPMediaItemPropertyArtwork];
      
          ...
      
          //設(shè)置到系統(tǒng)
          [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];
      }
      
      - (void)setDelegate:(id<MusicPlayerManagerDelegate>)delegate{
          _delegate = delegate;
          if (_delegate) {
              //有代理
              
              //判斷是否有音樂在播放
              if ([self isPlaying]) {
                  //有音樂在播放
                  
                  //啟動定時器
                  [self startPublishProgress];
              }
          } else {
              //沒有代理
              
              //停止定時器
              [self stopPublishProgress];
          }
      }
      @end
      

      音樂列表邏輯封裝到MusicListManager:

      @implementation MusicListManager
      static MusicListManager *sharedInstance = nil;
      
      - (instancetype)init
      {
          self = [super init];
          if (self) {
              _datum=[[NSMutableArray alloc] init];
              
              //初始化音樂播放管理器
              self.musicPlayerManager=[MusicPlayerManager shared];
              
              __weak typeof(self)weakSelf = self;
              
              //設(shè)置播放完畢回調(diào)
              [self.musicPlayerManager setComplete:^(Song * _Nonnull data) {
                  
                  //判斷播放循環(huán)模式
                  if ([weakSelf getLoopModel] == MusicPlayRepeatModelOne) {
                      //單曲循環(huán)
                      [weakSelf play:weakSelf.data];
                  } else {
                      //其他模式
                      [weakSelf play:[weakSelf next]];
                  }
              }];
              
              self.model=MusicPlayRepeatModelList;
              
              [self initPlayList];
          }
          return self;
      }
      
      /// 獲取單例對象
      +(instancetype)shared{
          if (!sharedInstance) {
              sharedInstance = [[self alloc] init];
          }
          return sharedInstance;
      }
      
      /// 設(shè)置默認(rèn)播放音樂
      -(void)defaultPlaySong{
          _data=_datum[0];
      }
      
      /// 設(shè)置播放列表
      - (void)setDatum:(NSArray *)datum{
          //將原來數(shù)據(jù)playList標(biāo)志設(shè)置為false
          [DataUtil changePlayListFlag:_datum inList:NO];
      
          //保存到數(shù)據(jù)庫
          [self saveAll];
      
          //清空原來的數(shù)據(jù)
          [_datum removeAllObjects];
      
          //添加新的數(shù)據(jù)
          [_datum addObjectsFromArray:datum];
      
          //更改播放列表標(biāo)志
          [DataUtil changePlayListFlag:_datum inList:YES];
      
          //保存到數(shù)據(jù)庫
          [self saveAll];
      
          [self sendMusicListChanged];
      }
      
      /// 保存當(dāng)前播放列表到數(shù)據(jù)庫
      -(void)saveAll{
          [[SuperDatabaseManager shared] saveAllSong:_datum];
      }
      
      -(void)sendMusicListChanged{
          MusicListChangedEvent *event = [[MusicListChangedEvent alloc] init];
          [QTEventBus.shared dispatch:event];
      }
      
      /**
       * 獲取播放列表
       */
      - (NSArray *)getDatum{
          return _datum;
      }
      
      /**
       * 播放
       */
      - (void)play:(Song *)data{
          self.data = data;
          
          //標(biāo)記為播放了
          self.isPlay = YES;
          
          NSString *path;
          
          //查詢是否有下載任務(wù)
          DownloadInfo *downloadInfo=[[AppDelegate.shared getDownloadManager] findDownloadInfo:data.id];
          if (downloadInfo != nil && downloadInfo.status == DownloadStatusCompleted) {
              //下載完成了
      
              //播放本地音樂
              path = [[StorageUtil documentUrl] URLByAppendingPathComponent:downloadInfo.path].path;
      
              LogDebugTag(MusicListManagerTag, @"MusicListManager play offline:%@ %@",path,data.uri);
          } else {
              //播放在線音樂
              path = [ResourceUtil resourceUri:data.uri];
      
              LogDebugTag(MusicListManagerTag, @"MusicListManager play online:%@ %@",path,data.uri);
          }
          
          [_musicPlayerManager play:path data:data];
          
          //設(shè)置最后播放音樂的Id
          [PreferenceUtil setLastPlaySongId:_data.id];
      }
      
      /**
       * 暫停
       */
      - (void)pause{
          LogDebugTag(MusicListManagerTag, @"pause");
          [_musicPlayerManager pause];
      }
      
      ...
      
      /// 更改循環(huán)模式
      - (MusicPlayRepeatModel)changeLoopModel{
          //循環(huán)模式+1
          _model++;
      
          //判斷循環(huán)模式邊界
          if (_model > MusicPlayRepeatModelRandom) {
              //如果當(dāng)前循環(huán)模式
              //大于最后一個循環(huán)模式
              //就設(shè)置為第0個循環(huán)模式
              _model = MusicPlayRepeatModelList;
          }
          
          //返回最終的循環(huán)模式
          return _model;
      }
      
      /**
       * 獲取循環(huán)模式
       */
      - (MusicPlayRepeatModel)getLoopModel{
          return _model;
      }
      
      - (Song *)getData{
          return self.data;
      }
      
      /**
       * 獲取上一個
       */
      - (Song *)previous{
          //音樂索引
          NSUInteger index = 0;
      
          //判斷循環(huán)模式
          switch (self.model) {
              case MusicPlayRepeatModelRandom:{
                  //隨機(jī)循環(huán)
      
                  //在0~datum.size()中
                  //不包含datum.size()
                  index = arc4random() % [_datum count];
              }
                  break;
              default:{
                  //找到當(dāng)前音樂索引
                  index = [_datum indexOfObject:self.data];
      
                  if (index != -1) {
                      //找到了
      
                      //如果當(dāng)前播放是列表第一個
                      if (index == 0) {
                          //第一首音樂
      
                          //那就從最后開始播放
                          index = [_datum count] - 1;
                      } else {
                          index--;
                      }
                  } else {
                      //拋出異常
                      //因為正常情況下是能找到的
                      
                  }
              }
                  break;
          }
      
          //獲取音樂
          return [_datum objectAtIndex:index];
      }
      
      ...
      @end
      

      外界統(tǒng)一使用播放列表管理器播放音樂,上一曲下一曲:

      -(void)onLoopModelClick:(UIButton *)sender{
          //更改循環(huán)模式
          [[MusicListManager shared] changeLoopModel];
      
          //顯示循環(huán)模式
          [self showLoopModel];
      
      }
      
      -(void)onPreviousClick:(UIButton *)sender{
          [[MusicListManager shared] play: [[MusicListManager shared] previous]];
      }
      
      -(void)onPlayClick:(UIButton *)sender{
          [self playOrPause];
      }
      
      /// 播放或暫停
      -(void)playOrPause{
          if ([[MusicPlayerManager shared] isPlaying]) {
              [[MusicListManager shared] pause];
          } else {
              [[MusicListManager shared] resume];
          }
      }
      
      -(void)onNextClick:(UIButton *)sender{
          [[MusicListManager shared] play: [[MusicListManager shared] next]];
      }
      

      歌詞

      歌詞實(shí)現(xiàn)了LRC,KSC兩種歌詞,封裝到LyricListView,單個歌詞行封裝到LyricView中,外界直接使用LyricListView就行:

      /// 顯示歌詞數(shù)據(jù)
      -(void)showLyricData{
          _lyricView.data = [[MusicListManager shared] getData].parsedLyric;
      }
      

      歌詞控件封裝:

      @implementation LyricListView
      
      - (instancetype)init{
          self=[super init];
          
          self.datum = [NSMutableArray array];
          
          [self initViews];
          
          return self;
      }
      
      - (void)initViews{
          //設(shè)置約束
          self.myWidth = MyLayoutSize.fill;
          self.myHeight = MyLayoutSize.fill;
          
          //tableView
          self.tableView = [ViewFactoryUtil tableView];
          self.tableView.delegate = self;
          self.tableView.dataSource = self;
          [self addSubview:self.tableView];
          
          //注冊歌詞cell
          [self.tableView registerClass:[LyricCell class] forCellReuseIdentifier:Cell];
          
          //創(chuàng)建一個水平方向容器
          _lyricDragContainer = [[MyLinearLayout alloc] initWithOrientation:MyOrientation_Horz];
          _lyricDragContainer.visibility = MyVisibility_Gone;
          _lyricDragContainer.myHorzMargin = PADDING_OUTER;
          _lyricDragContainer.myWidth = MyLayoutSize.fill;
          _lyricDragContainer.myHeight = MyLayoutSize.wrap;
      
          ...
      
          //分割線
          UIView *dividerView = [ViewFactoryUtil smallDivider];
          dividerView.weight=1;
          dividerView.backgroundColor = [UIColor colorLightWhite];
          [_lyricDragContainer addSubview:dividerView];
      
          //時間
          _timeView = [UILabel new];
          _timeView.myWidth = MyLayoutSize.wrap;
          _timeView.myHeight = MyLayoutSize.wrap;
          _timeView.text = @"00:00";
          _timeView.textColor = [UIColor colorLightWhite];
          [_lyricDragContainer addSubview:_timeView];
      }
      
      - (void)setData:(Lyric *)data{
          _data=data;
          
          if (_lyricPlaceholderSize > 0) {
              //已經(jīng)計算了填充數(shù)量
              [self next];
          }
      }
      
      - (void)next{
          //清空原來的歌詞
          [_datum removeAllObjects];
      
          if (_data) {
              //添加占位數(shù)據(jù)
              [self addLyricFillData];
              [_datum addObjectsFromArray:_data.datum];
      
              //添加占位數(shù)據(jù)
              [self addLyricFillData];
          }
      
          _isReloadData=YES;
          [_tableView reloadData];
      }
      
      /// 添加歌詞占位數(shù)據(jù)
      /// 添加的目的是讓第一行歌詞也能顯示到控件垂直方向中心
      -(void)addLyricFillData {
          for (int i=0; i<_lyricPlaceholderSize; i++) {
              [_datum addObject:@"fill"];
          }
      }
      
      - (void)setProgress:(float)progress{
          if(!_isReloadData && _lyricPlaceholderSize > 0){
              //還沒有加載數(shù)據(jù)
              
              //所以這里加載數(shù)據(jù)
              [self next];
          }
          
          if (_data && _datum.count>0) {
              if (_isDrag) {
                 //正在拖拽歌詞
                 //就直接返回
                 return;
              }
              
              //獲取當(dāng)前時間對應(yīng)的歌詞索引
              NSInteger newLineNumber = [LyricUtil getLineNumber:_data progress:progress] + _lyricPlaceholderSize;
      
              if (newLineNumber != _lyricLineNumber) {
                 //滾動到當(dāng)前行
                 [self scrollPosition:newLineNumber];
      
                 _lyricLineNumber = newLineNumber;
              }
              
              //如果是精確到字歌曲
             //還需要將時間分發(fā)到item中
             //因為要持續(xù)繪制
             if (_data.isAccurate) {
                 NSObject *object = _datum[_lyricLineNumber];
                 if ([object isKindOfClass:[LyricLine class]]) {
                     //只有是歌詞行才處理
      
                     //獲取當(dāng)前時間是該行的第幾個字
                     NSInteger lyricCurrentWordIndex=[LyricUtil getWordIndex:object progress:progress];
      
                     //獲取當(dāng)前時間改字
                     //已經(jīng)播放的時間
                     NSInteger wordPlayedTime=[LyricUtil getWordPlayedTime:object progress:progress];
      
                     //獲取cell
                     LyricCell *cell= [self getCell:self.lyricLineNumber];
      
                     if (cell) {
                         //有可能獲取不到當(dāng)前位置的Cell
                         //因為上面使用了滾動動畫
                         //如果不使用滾動動畫效果不太好
      
                         //將當(dāng)前時間對應(yīng)的字索引設(shè)置到控件
                         [cell.lineView setLyricCurrentWordIndex:lyricCurrentWordIndex];
      
                         //設(shè)置當(dāng)前字已經(jīng)播放的時間
                         [cell.lineView setWordPlayedTime:wordPlayedTime];
      
                         //標(biāo)記需要繪制
                         [cell.lineView setNeedsDisplay];
                     }
      
                 }
             }
          }
      }
      
      ...
      
      #pragma mark - 列表數(shù)據(jù)源
      /// 有多少個
      /// @param tableView <#tableView description#>
      /// @param section <#section description#>
      - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
          return _datum.count;
      }
      
      /// 返回當(dāng)前位置的cell
      /// @param tableView <#tableView description#>
      /// @param indexPath <#indexPath description#>
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
          //獲取cell
          LyricCell *cell=[tableView dequeueReusableCellWithIdentifier:Cell forIndexPath:indexPath];
          
          //設(shè)置Tag
          cell.tag = indexPath.row;
          
          //取出數(shù)據(jù)
          NSObject *data = _datum[indexPath.row];
          
          //綁定數(shù)據(jù)
          [cell bind:data accurate:_data.isAccurate];
          
          //返回cell
          return cell;
      }
      
      #pragma mark - 滾動相關(guān)
      
      /// 開始拖拽時調(diào)用
      /// @param scrollView <#scrollView description#>
      - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
          LogDebugTag(LyricListViewTag, @"scrollViewWillBeginDragging");
          [self showDragView];
      }
      
      /// 拖拽結(jié)束
      /// @param scrollView <#scrollView description#>
      /// @param decelerate <#decelerate description#>
      - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
          NSLog(@"lyric view scrollViewDidEndDragging:%d",decelerate);
      
          if (!decelerate) {
              //如果不需要減速,就延時后,顯示歌詞
              [self prepareScrollLyricView];
          }
      }
      
      /// 滾動結(jié)束(慣性滾動)
      /// @param scrollView <#scrollView description#>
      - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
          NSLog(@"lyric view scrollViewDidEndDecelerating");
          //如果需要減速,在這里延時后,顯示歌詞
          [self prepareScrollLyricView];
      }
      
      ...
      @end
      

      控制器

      使用了可以通過系統(tǒng)媒體控制器,通知欄,鎖屏界面,耳機(jī),藍(lán)牙耳機(jī)等設(shè)備控制媒體播放暫停,只需要把媒體信息更新到系統(tǒng):

      - (void)setMediaInfo:(UIImage *)image{
          //初始化一個可變字典
          NSMutableDictionary *songInfo=[[NSMutableDictionary alloc] init];
      
          //初始化一個封面
          MPMediaItemArtwork *albumArt=[[MPMediaItemArtwork alloc] initWithBoundsSize:image.size requestHandler:^UIImage * _Nonnull(CGSize size) {
              return image;
          }];
      
          //設(shè)置封面
          [songInfo setObject:albumArt forKey:MPMediaItemPropertyArtwork];
      
          //歌曲名稱
          [songInfo setObject:self.data.title forKey:MPMediaItemPropertyTitle];
      
          ...
      
          //設(shè)置到系統(tǒng)
          [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];
      }
      

      媒體控制

      /// 接收遠(yuǎn)程音樂播放控制消息
      /// 例如:點(diǎn)擊耳機(jī)上的按鈕,點(diǎn)擊媒體控制中心按鈕等
      /// @param event <#event description#>
      - (void)remoteControlReceivedWithEvent:(UIEvent *)event{
          //判斷是不是遠(yuǎn)程控制事件
          if (event.type == UIEventTypeRemoteControl) {
              if ([[MusicListManager shared] getData] == nil) {
                  //當(dāng)前播放列表中沒有音樂
                  return;
              }
      
              //判斷事件類型
              switch (event.subtype) {
                  case UIEventSubtypeRemoteControlPlay:{
                      //點(diǎn)擊了播放按鈕
                      [[MusicListManager shared] resume];
                      NSLog(@"AppDelegate play");
                  }
                      break;
                  case UIEventSubtypeRemoteControlPause:{
                      //點(diǎn)擊了暫停
                      [[MusicListManager shared] pause];
                      NSLog(@"AppDelegate pause");
                  }
                      break;
                  case UIEventSubtypeRemoteControlNextTrack:{
                      //下一首
                      //雙擊iPhone有線耳機(jī)上的控制按鈕
                      Song *song = [[MusicListManager shared] next];
                      [[MusicListManager shared] play:song];
                      NSLog(@"AppDelegate Next");
                  }
                      break;
                  ...
                  default:
                      break;
              }
          }
      }
      

      登錄/注冊/驗證碼登錄

      i13.png

      登錄注冊沒有多大難度,用戶名和密碼登錄,就是把信息傳遞到服務(wù)端,可以加密后在傳輸,服務(wù)端判斷登錄成功,返回一個標(biāo)記,客戶端保存,其他需要的登錄的接口帶上;驗證碼登錄就是用驗證碼代替密碼,發(fā)送驗證碼都是服務(wù)端發(fā)送,客戶端只需要調(diào)用接口。

      評論

      i14.png
      評論列表包括下拉刷新,上拉加載更多,點(diǎn)贊,發(fā)布評論,回復(fù)評論,Emoji,話題和提醒人點(diǎn)擊,選擇好友,選擇話題等。

      刷新和下拉加載更多

      核心邏輯就只需要更改page就行了

      //下拉刷新
      MJRefreshNormalHeader *header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{
          @strongify(self);
          [self loadData];
      }];
      
      //隱藏標(biāo)題
      header.stateLabel.hidden = YES;
      
      // 隱藏時間
      header.lastUpdatedTimeLabel.hidden = YES;
      self.tableView.mj_header=header;
      
      //上拉加載更多
      MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
          @strongify(self);
          [self loadMore];
      }];
      
      // 設(shè)置空閑時文字
      [footer setTitle:@"" forState:MJRefreshStateIdle];
      
      self.tableView.mj_footer = footer;
      

      人和話題點(diǎn)擊

      通過正則表達(dá)式,找到特殊文本,然后使用富文本實(shí)現(xiàn)點(diǎn)擊。

      /// 處理文本點(diǎn)擊事件
      /// 這部分可以用監(jiān)聽器回調(diào)到界面處理
      /// @param data <#data description#>
      -(NSAttributedString *)processContent:(NSString *)data{
          return [RichUtil processContent:data mentionClick:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
              NSString *clickText = [RichUtil processClickText:data range:range];
              LogDebugTag(CommentCellTag, @"processContent mention click %@",clickText);
              
              if (self.nicknameClickBlock) {
                  self.nicknameClickBlock(clickText);
              }
          } hashTagClick:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
              NSString *clickText = [RichUtil processClickText:data range:range];
              LogDebugTag(CommentCellTag, @"processContent hash click %@",clickText);
              
              if (self.TagClickBlock) {
                  self.TagClickBlock(clickText);
              }
          }];
      }
      

      好友

      @implementation UserController
      
      - (void)initViews{
          [super initViews];
          
          //初始化TableView結(jié)構(gòu)
          [self initTableViewSafeArea];
          
          [self.tableView registerClass:[TopicCell class] forCellReuseIdentifier:Cell];
      }
      
      - (void)initDatum{
          [super initDatum];
          
          if (self.style==StyleFriend || self.style==StyleSelect) {
              //好友
              [self setTitle:R.string.localizable.myFriend];
          } else {
              //粉絲
              [self setTitle:R.string.localizable.myFans];
          }
          
          [self loadData];
      }
      
      - (void)loadData:(BOOL)isPlaceholder{
          DefaultRepository *repository=[DefaultRepository shared];
          
          if (self.style==StyleFriend || self.style==StyleSelect) {
              //好友
              [repository friends:[PreferenceUtil getUserId] success:^(BaseResponse * _Nonnull baseResponse, Meta * _Nonnull meta, NSArray * _Nonnull data) {
                  [self show:data];
              }];
          } else {
              //粉絲
              [repository fans:[PreferenceUtil getUserId] success:^(BaseResponse * _Nonnull baseResponse, Meta * _Nonnull meta, NSArray * _Nonnull data) {
                  [self show:data];
              }];
          }
      }
      
      -(void)show:(NSArray *)data{
          [self.datum removeAllObjects];
          [self.datum addObjectsFromArray:data];
          [self.tableView reloadData];
      }
      
      #pragma mark - 列表數(shù)據(jù)源
      
      /// 返回當(dāng)前位置的cell
      /// 相當(dāng)于Android中RecyclerView Adapter的onCreateViewHolder
      /// @param tableView <#tableView description#>
      /// @param indexPath <#indexPath description#>
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
          
          User *data= self.datum[indexPath.row];
          
          TopicCell *cell=[tableView dequeueReusableCellWithIdentifier:Cell forIndexPath:indexPath];
          
          [cell bindWithUser:data];
          
          return cell;
          
      }
      
      - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
          User *data=self.datum[indexPath.row];
          
          if (self.style==StyleSelect) {
              //選擇
              SelectUserEvent *event = [[SelectUserEvent alloc] init];
              event.data=data;
              [QTEventBus.shared dispatch:event];
              
              [self finish];
          }else{
              
              [UserDetailController start:self.navigationController id:data.id];
          }
      }
      
      #pragma mark - 啟動界面
      +(void)start:(UINavigationController *)controller style:(ListStyle)style{
          UserController *target=[UserController new];
          target.style=style;
          [controller pushViewController:target animated:YES];
      }
      
      @end
      

      視頻和播放

      i15.png

      真實(shí)項目中視頻播放大部分都是用第三方服務(wù),例如:阿里云視頻服務(wù),騰訊視頻服務(wù),因為他們提供一條龍服務(wù),包括審核,轉(zhuǎn)碼,CDN,安全,播放器等,這里用不到這么多功能,所以使用了第三方播放器播放普通mp4,這使用餃子播放器框架。

      -(void)play:(Video *)data{
      //    //不開防盜鏈
      //    SuperPlayerModel *model = [[SuperPlayerModel alloc] init];
      //
      //    //播放騰訊云視頻
      //    // 配置 AppId
      ////    model.appId = 0;
      ////
      ////    model.videoId = [[SuperPlayerVideoId alloc] init];
      ////    model.videoId.fileId = "5285890799710670616"; // 配置 FileId
      //
      //    //停止播放
      //    [_playerView removeVideo];
      //
      //    //直接使用url播放
      //    model.videoURL = [ResourceUtil resourceUri:data.uri];
      //
      //    [_playerView playWithModel:model];
      //
      //    //設(shè)置標(biāo)題
      //    [self.playerView.controlView setTitle:data.title];
      }
      

      用戶詳情/更改資料

      i16.png

      用戶詳情頂部顯示用戶信息,好友數(shù)量,下面分別顯示創(chuàng)建的歌單,收藏的歌單,發(fā)布的動態(tài),類似微信朋友圈,右上角可以更改用戶資料;使用第三方框架里面的kJXPagingListRefreshView控件實(shí)現(xiàn)。

      -(void)initUI{
          [self.container removeAllSubviews];
          
          //頭部控件
          _userHeaderView = [[UserDetailHeaderView alloc] init];
          
          [_userHeaderView setFollowBlock:^{
              [self loginAfter:^{
                  [self onFollowClick];
              }];
          }];
          
          [_userHeaderView setSendMessageBlock:^{
              [ChatController start:self.navigationController id:self.data.id];
          }];
          
          //指示器
          _categoryView = [[JXCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, SIZE_INDICATOR_HEIGHT)];
          
          //標(biāo)題
          self.categoryView.titles = @[R.string.localizable.sheet, R.string.localizable.feed];
          
          self.categoryView.backgroundColor = [UIColor clearColor];
          self.categoryView.delegate = self;
          
          //選擇的顏色
          self.categoryView.titleSelectedColor = [UIColor colorPrimary];
          
          //默認(rèn)顏色
          self.categoryView.titleColor = [UIColor colorOnSurface];
          
          //選中是否放大
          self.categoryView.titleLabelZoomEnabled = NO;
      
          //指示器下面那條線
          JXCategoryIndicatorLineView *lineView = [[JXCategoryIndicatorLineView alloc] init];
          
          //選中顏色
          lineView.indicatorColor = [UIColor colorPrimary];
          lineView.indicatorWidth = 30;
          self.categoryView.indicators = @[lineView];
          
          self.pagerView = [[JXPagerListRefreshView alloc] initWithDelegate:self];
          self.pagerView.mainTableView.gestureDelegate = self;
          self.pagerView.myWidth=MyLayoutSize.fill;
          self.pagerView.myHeight=MyLayoutSize.fill;
          [self.container addSubview:self.pagerView];
      
          self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;
      }
      

      然后就是把每個子界面放到單獨(dú)View中,并在代理方法返回就行了。

      發(fā)布動態(tài)/選擇位置/路徑規(guī)劃

      i17.png
      發(fā)布效果和微信朋友圈類似,可以選擇圖片,和地理位置;地理位置使用高德地圖實(shí)現(xiàn)選擇,路徑規(guī)劃是調(diào)用系統(tǒng)中安裝的地圖,類似微信。

      位置

      /// 搜索該位置的poi,方便用戶選擇,也方便其他人找
      -(void)searchPOI{
      	//LogDebug(@"searchPOI %f %f %@",data.);
          if (_keyword) {
              //關(guān)鍵字搜索
              AMapPOIKeywordsSearchRequest *request = [AMapPOIKeywordsSearchRequest new];
              
              //關(guān)鍵字
              request.keywords=_keyword;
      
              //距離排序
              request.sortrule = 0;
      
              //是否返回擴(kuò)展信息
              request.requireExtension=YES;
      
              [self.search AMapPOIKeywordsSearch:request];
              
          } else {
              //搜索位置附近
              AMapPOIAroundSearchRequest *request = [AMapPOIAroundSearchRequest new];
              request.location=[AMapGeoPoint locationWithLatitude:_coordinate.latitude longitude:_coordinate.longitude];
              
              //距離排序
              request.sortrule=0;
              
              //是否返回擴(kuò)展信息
              request.requireExtension=YES;
              
              [self.search AMapPOIAroundSearch:request];
          }
      }
      

      地圖路徑規(guī)劃

      + (void)amapPathPlan:(NSString *)title latitude:(double)latitude longitude:(double)longitude{
          NSString *result=[NSString stringWithFormat:@"iosamap://path?sourceApplication=我的云音樂&backScheme=weichat&dlat=%f&dlon=%f&dname=%@",latitude,longitude,title];
          [SuperApplicationUtil open:result];
      }
      

      聊天/離線推送

      i18.png
      大部分真實(shí)項目中聊天都會選擇第三方商業(yè)級付費(fèi)聊天服務(wù),常用的有騰訊云聊天,融云聊天,網(wǎng)易云聊天等,這里選擇融云聊天服務(wù),使用步驟是先在服務(wù)端生成聊天Token,這里是登錄后返回,然后客戶端登錄聊天服務(wù)器,然后設(shè)置消息監(jiān)聽,發(fā)送消息等。

      聊天服務(wù)器

      /// 連接聊天服務(wù)器
      /// @param data <#data description#>
      -(void)connectChat:(Session *)data{
          [[RCIMClient sharedRCIMClient] connectWithToken:data.chatToken dbOpened:^(RCDBErrorCode code) {
                      //消息數(shù)據(jù)庫打開,可以進(jìn)入到主頁面
                  } success:^(NSString *userId) {
                      //連接成功
                  } error:^(RCConnectErrorCode status) {
                      if (status == RC_CONN_TOKEN_INCORRECT) {
                          //從 APP 服務(wù)獲取新 token,并重連
                      } else {
                          //無法連接到 IM 服務(wù)器,請根據(jù)相應(yīng)的錯誤碼作出對應(yīng)處理
                      }
      
                      //因為我們這個應(yīng)用,不是類似微信那樣純聊天應(yīng)用,所以聊天服務(wù)器連接失敗,也讓進(jìn)入應(yīng)用
                      //真實(shí)項目中按照需求實(shí)現(xiàn)就行了
                      [SuperToast showWithTitle:R.string.localizable.errorMessageLogin];
                  }];
      }
      

      消息監(jiān)聽

      - (void)onReceived:(RCMessage *)message left:(int)nLeft object:(id)object{
          dispatch_async(dispatch_get_main_queue(), ^{
              //切換到主線程
              
              if ([message.targetId isEqualToString:self.currentChatUserId]) {
                  //正在和這個人聊天
              }else{
                  //其他消息顯示到通知欄
                  [NotificationUtil showMessage:message];
              }
              
              //發(fā)送消息到通知(這個通知是,跨界面通訊,不是顯示到通知欄)
              [NSNotificationCenter.defaultCenter postNotificationName:ON_MESSAGE object:nil userInfo:@{@"data":message}];
              
              //發(fā)送消息未讀數(shù)改變了通知
              [NSNotificationCenter.defaultCenter postNotificationName:ON_MESSAGE_COUNT_CHANGED object:nil userInfo:nil];
          });
      }
      

      文本消息

      發(fā)送圖片等其他消息也是差不多。

      /// 發(fā)送文本消息
      -(void)sendTextMessage{
          NSString *result=_contentInputView.text;
          
          if([StringUtil isBlank:result]){
              [SuperToast showWithTitle:R.string.localizable.hintEnterMessage];
              return;
          }
          
          //1.構(gòu)造文本消息
          RCTextMessage *txtMsg = [RCTextMessage messageWithContent:result];
      
          //2.將文本消息發(fā)送出去
          [[RCIMClient sharedRCIMClient] sendMessage:ConversationType_PRIVATE
          targetId:self.id
          content:txtMsg
          pushContent:nil
          pushData:[MessageUtil createPushData:[MessageUtil getContent:txtMsg] targetId:[PreferenceUtil getUserId]]
          success:^(long messageId) {
      
              NSLog(@"消息發(fā)送成功,message id 為 %@",@(messageId));
      
              dispatch_async(dispatch_get_main_queue(), ^{
                  //清空輸入框
                  [self clearInput];
              });
      
              [self addMessage:[[RCIMClient sharedRCIMClient] getMessage:messageId]];
      
          } error:^(RCErrorCode nErrorCode, long messageId) {
      
              NSLog(@"消息發(fā)送失敗,錯誤碼 為 %@",@(nErrorCode));
              
          }];
      }
      

      離線推送

      需要付費(fèi)蘋果開發(fā)者賬戶,先開啟SDK離線推送,然后在蘋果開發(fā)者后臺創(chuàng)建推送證書,配置到融云,最后在代碼中處理通知點(diǎn)擊等。

      /// 界面已經(jīng)顯示了
      /// @param animated <#animated description#>
      - (void)viewDidAppear:(BOOL)animated{
          [super viewDidAppear:animated];
      
          //延時的目的是讓當(dāng)前界面顯示出來以后,在檢查
          //檢查是否需要處理通知點(diǎn)擊
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
              //檢查是否需要處理通知點(diǎn)擊
              [self checkProcessNotificationClick];
           });
      }
      
      /// 檢查是否需要處理通知點(diǎn)擊
      -(void)checkProcessNotificationClick{
          if ([AppDelegate shared].pushData) {
              [self processPushClick:[AppDelegate shared].pushData];
      
              [AppDelegate shared].pushData=nil;
          }
      }
      

      商城/訂單/支付/購物車

      i2011.png

      i2012.png

      學(xué)到這里,大家不能說熟悉,那么看到上面的界面,那么大體要能實(shí)現(xiàn)出來。

      詳情富文本

      //詳情
      self.detailView = [QMUITextView new];
      self.detailView.myWidth = MyLayoutSize.fill;
      self.detailView.myHeight = MyLayoutSize.wrap;
      self.detailView.delegate=self;
      self.detailView.scrollEnabled=NO;
      self.detailView.editable=NO;
      
      //去除左右邊距
      self.detailView.textContainer.lineFragmentPadding = 0;
      
      //去除上下邊距
      self.detailView.textContainerInset = UIEdgeInsetsZero;
      [self.contentContainer addSubview:self.detailView];
      

      寶/微信支付

      客戶端先集成微信,支付寶SDK,然后請求服務(wù)端獲取支付信息,設(shè)置到SDK,最后就是處理支付結(jié)果。

      /// 處理支付寶支付
      /// @param data <#data description#>
      - (void)processAlipay:(NSString *)data{
          //支付寶官方開發(fā)文檔:https://docs.open.alipay.com/204/105295/
          [[AlipaySDK defaultService] payOrder:data fromScheme:ALIPAY_CALLBACK_SCHEME callback:^(NSDictionary *resultDic) {
              //如果手機(jī)中沒有安裝支付寶客戶端
              //會跳轉(zhuǎn)H5支付頁面
              //支付相關(guān)的信息會通過這個方法回調(diào)
      
              //處理支付寶支付結(jié)果
              [self processAlipayResult:resultDic];
          }];
      }
      

      支付結(jié)果

      /// 處理支付寶支付結(jié)果
      /// @param data <#data description#>
      - (void)processAlipayResult:(NSDictionary *)data{
          NSString *resultStatus=data[@"resultStatus"];
      
          if ([@"9000" isEqualToString:resultStatus]) {
              //本地支付成功
      
              //不能依賴本地支付結(jié)果
              //一定要以服務(wù)端為準(zhǔn)
              [SuperToast showLoading:R.string.localizable.hintPayWait];
      
              [self checkPayStatus];
              
              //這里就不根據(jù)服務(wù)端判斷了
              //購買成功統(tǒng)計
              [AnalysisUtil onPurchase:YES data:self.data];
          }if ([@"6001" isEqualToString:resultStatus]) {
              //取消了
              [self showCancel];
          } else {
              //支付失敗
              [self showPayFailedTip];
          }
      }
      

      項目總結(jié)

      總體來說項目功能還是很全的,還有一些小功能,例如:快捷方式等就不在貼代碼了,但肯定沒發(fā)和原版比,相信大家只要做過程序員就能理解,畢竟原版是一個商業(yè)級項目,幾十個人天天開發(fā)和維護(hù),而且持續(xù)了幾年了;不過恕我直言,現(xiàn)在的常見的音樂軟件都太復(fù)雜了,各種功能,不過都要恰飯,好像又能理解了??。

      posted on 2022-08-10 16:08  愛學(xué)啊  閱讀(670)  評論(0)    收藏  舉報

      導(dǎo)航

      主站蜘蛛池模板: 国产蜜臀一区二区在线播放| 黑巨人与欧美精品一区| 欧美人与性动交α欧美精品| 亚洲天天堂天堂激情性色| 兰州市| 亚洲狠狠狠一区二区三区| 亚洲春色在线视频| 亚洲中文字幕国产综合| 国产最新AV在线播放不卡| 日韩不卡1卡2卡三卡网站| 亚洲经典av一区二区| 国产视色精品亚洲一区二区| 日本中文字幕乱码免费| 国产在线拍揄自揄拍无码视频 | 亚洲欧美中文日韩V日本| 国产成人高清在线重口视频| 三上悠亚精品一区二区久久| 亚洲精品一区二区三区大桥未久| 日韩av第一页在线播放| 日韩人妻精品中文字幕| 亚洲国产成人精品av区按摩| 欧美成人aaa片一区国产精品 | 中文字幕久久国产精品| 日韩av综合免费在线| 内射中出无码护士在线| 91福利国产成人精品导航| 人妻伦理在线一二三区| 欧美不卡一区二区三区| 亚洲中文字幕日韩精品| 又大又紧又粉嫩18p少妇| 国产精品亚洲二区在线播放| 日本高清不卡一区二区三| 亚洲а∨天堂久久精品2021| 成人啪啪高潮不断观看| 阿巴嘎旗| 无遮无挡爽爽免费视频| 中文字幕无码视频手机免费看| 久久综合色之久久综合| 成A人片亚洲日本久久| 日韩精品一区二区三区在线观看| 可以在线观看的亚洲视频|