• OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM


    效果

    在这里插入图片描述

    因为OC版本大部分截图和Swift版本一样,所以就不再另外截图了。

    列文章目录

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

    目简介

    这是一个使用OC语言(还有Swift,Android版本),从0开发一个iOS平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识;主要是使用系统功能,流行的第三方框架,第三方服务,完成接近企业级商业级项目。

    目功能点

    隐私协议对话框
    启动界面和动态处理权限
    引导界面和广告
    轮播图和侧滑菜单
    首页复杂列表和列表排序
    音乐播放和音乐列表管理
    全局音乐控制条
    桌面歌词和自定义样式
    全局媒体控制中心
    评论和回复评论
    评论富文本点击
    评论提醒人和话题
    朋友圈动态列表和发布
    高德地图定位和路径规划
    阿里云OSS上传
    视频播放和控制
    QQ/微信登录和分享
    商城/购物车\微信\支付宝支付
    文本和图片聊天
    消息离线推送
    自动和手动检查更新
    内存泄漏和优化

    发环境概述

    2022年5月开发完成的,所以全部都是最新的,平均每3年会重新制作,现在已经是第三版了。

    Xcode 13.4
    iOS 15
    
    • 1
    • 2

    译和运行

    先安装pod,用最新Xcode打开MyCloudMusic.xcworkspace,然后运行,如果要运行到真机,先登陆自己的开发者账户,如果不是付费账户,请删除推送等付费功能,更改BundleId,然后运行。

    目目录结构

    ├── MyCloudMusic
    │   ├── AppDelegate.h
    │   ├── AppDelegate.m
    │   ├── Assets.xcassets #资源目录
    │   ├── Base.lproj
    │   ├── Cell #通用cell
    │   ├── Component #每个功能模块
    │   │   ├── Ad #广告相关
    │   │   ├── Address #收货地址相关
    │   ├── Config #配置目录,例如:网络地址配置
    │   ├── Controller #通用控制器
    │   ├── Extension #扩展,例如:字符串扩展
    │   ├── Info.plist
    │   ├── Manager #管理器,例如:音乐播放管理器
    │   ├── Model  #通用模型
    │   ├── MyCloudMusic.entitlements
    │   ├── Network
    │   ├── PrefixHeader.pch
    │   ├── Repository #数据仓库,例如:网络请求封装
    │   ├── 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    赖框架

    内容太多,只列出部分。

    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的自由启发,获取自动完成的本地化字符串、资产目录图像名称和故事板对象
      pod 'R.objc'
      
      #轮播图
      #https://github.com/QuintGao/GKCycleScrollView
      pod 'GKCycleScrollView'
      
      #网络框架
      #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'
      
      #极光统计
      #https://docs.jiguang.cn/janalytics/guideline/intro/
      pod 'JAnalytics'
      
      #webview和js交互框架
      #可以直接使用系统提供的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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    户协议对话框

    使用自定义Dialog实现。

    @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];
        
        //内容容器
        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];
        
        //标题
        [self.contentContainer addSubview:self.titleView];
        
        self.textView=[UITextView new];
        self.textView.myWidth=MyLayoutSize.fill;
        
        //超出的内容,自动支持滚动
        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;
        
        //点击外部不隐藏
        [self.modalController setModal:YES];
        
        //边距
        self.modalController.contentViewMargins=UIEdgeInsetsMake(PADDING_LARGE2, PADDING_LARGE2, PADDING_LARGE2, PADDING_LARGE2);
        
        //设置要显示的内容控件
        self.modalController.contentViewController=self;
        
        [self.modalController showWithAnimated:YES completion:nil];
    }
    
    - (void)hide{
        [self.modalController hideWithAnimated:YES completion:nil];
    }
    
    #pragma mark - 创建控件
    - (UILabel *)titleView{
        if (!_titleView) {
            _titleView=[UILabel new];
            _titleView.myWidth=MyLayoutSize.fill;
            _titleView.myHeight=MyLayoutSize.wrap;
            _titleView.text=@"标题";
            _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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    导界面

    在这里插入图片描述

    引导界面比较简单,就是多个图片可以左右滚动。

    @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  轮播图数据源
    
    /// 有多少个
    /// @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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    广告界面

    在这里插入图片描述

    实现图片广告和视频广告,广告数据是在首页是缓存到本地,目的是在启动界面加载更快,因为真实项目中,大部分项目启动页面广告时间一共就5秒,如果太长了用户体验不好,如果是从网络请求,那么网络可能就耗时2秒左右,所以导致就美哟多少时间显示广告了。

    广告

    func downloadAd(_ data:Ad,_ path:URL) {
        let destination: DownloadRequest.Destination = { _, _ in
            return (path, [.removePreviousFile, .createIntermediateDirectories])
        }
    
        AF.download(data.icon.absoluteUri(), to: destination).response { response in
            if response.error == nil, let filePath = response.fileURL?.path {
                print("ad downloaded success \(filePath)")
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    广告

    -(void)showVideoAd:(NSURL *)data{
        //播放应用内嵌入视频,放根目录中
        //同样其他的文件,也可以通过这种方式读取
    	//data = [NSBundle.mainBundle URLForResource:@"ixueaeduTestVideo" withExtension:@".mp4"];
    
        _player = [AVPlayer playerWithURL:data];
    
        //静音
        _player.muted = YES;
    
        /// 添加进度监听
        __weak typeof(self) weakSelf = self;
        [_player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            //当前时间,秒
            Float64 current=CMTimeGetSeconds(weakSelf.player.currentItem.currentTime);
    
            //总时间
            CGFloat duration =  CMTimeGetSeconds(weakSelf.player.currentItem.duration);
    
            if (current==duration) {
                //视频播放结束
                [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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    显示图片就是显示本地图片了,没什么难点,就不贴代码了。

    首页/歌单详情/黑胶唱片界面

    在这里插入图片描述

    首页没有顶部是轮播图,然后是可以左右的菜单,接下来是热门歌单,推荐单曲,最后是首页排序模块;整体上使用RecycerView实现,轮播图:

    //轮播图
    BannerCell *cell = [tableView dequeueReusableCellWithIdentifier:BannerCellName forIndexPath:indexPath];
    
    //绑定数据
    [cell bind:data];
    
    return cell;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    详情

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

    @implementation SheetDetailController
    
    - (void)initViews{
        [super initViews];
        //添加背景图片控件
        _backgroundImageView = [UIImageView new];
    
        //默认隐藏
        _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);
        
        //点击事件
        [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:^{
            //透明度设置为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) {
            //有音乐才设置
    
            //设置数据
            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{
        //把当前歌单所有音乐设置到播放列表
        //有些应用
        //可能会实现添加到已经播放列表功能
        [[MusicListManager shared] setDatum:self.data.songs];
    
        //播放当前音乐
        [[MusicListManager shared] play:data];
        
        [self startMusicPlayerController];
    }
    
    /// 有多少组
    /// @param tableView <#tableView description#>
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
        return self.datum.count;
    }
    
    /// 当前组有多少个
    /// @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;
        
        //取出组数据
        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];
        }];
    
        //绑定数据
        [header bind:groupData];
    
        //返回header
        return header;
    }
    
    /// 返回当前位置的cell
    /// 相当于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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187

    唱片

    上面是黑胶唱片,和网易云音乐差不多,随着音乐滚动或暂停,顶部是控制相关,音乐播放逻辑是封装到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];
            
            //默认状态
            self.status = PlayStatusNone;
        }
        return self;
    }
    
    - (void)play:(NSString *)uri data:(Song *)data{
        //设置音频会话
        [SuperAudioSessionManager requestAudioFocus];
        
        //更改播放状态
        _status = PlayStatusPlaying;
        
        //保存音乐对象
        self.data = data;
        
        NSURL *url=nil;
        if ([uri hasPrefix:@"http"]) {
            //网络地址
            url=[NSURL URLWithString:uri];
        } else {
            //本地地址
            url=[NSURL fileURLWithPath:uri];
        }
        
        //创建一个播放Item
        AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
        
        //替换掉原来的播放Item
        [self.player replaceCurrentItemWithPlayerItem:item];
        
        //播放
        [self.player play];
        
        ...
    }
    
    -(void)prepareLyric{
        //歌词处理
        //真实项目可能会
        //将歌词这个部分拆分到其他组件中
        if (_data.parsedLyric) {
            //解析好了
            [self onLyricReady];
        } else if(_data.lyric) {
            //有歌词,但是没有解析
            [self parseLyric];
        }else{
            //没有歌词,并且不是本地音乐才请求
    
            //真实项目中可以会缓存歌词
            //获取歌词数据
            [[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];
        }
        
        //通知歌词准备好了
        [self onLyricReady];
    }
    
    -(void)onLyricReady{
        if (self.delegate) {
            [self.delegate onLyricReady:_data];
        }
    }
    
    -(void)initListeners{
        //KVO方式监听播放状态
        //KVC:Key-Value Coding,另一种获取对象字段的值,类似字典
        //KVO:Key-Value Observing,建立在KVC基础上,能够观察一个字段值的改变
        [self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
        
        //监听音乐缓冲状态
        [self.player.currentItem addObserver:self
                                  forKeyPath:@"loadedTimeRanges"  options:NSKeyValueObservingOptionNew
                                     context:nil];
        
        //播放结束事件
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(onComplete:)
                                                     name:AVPlayerItemDidPlayToEndTimeNotification
                                                   object:self.player.currentItem];
    }
    
    /// 播放完毕了回调
    - (void)onComplete:(NSNotification *)notification {
        self.complete(_data);
    }
    
    /// 移除监听器
    -(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监听回调方法
    /// @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
    {
        //判断监听的字段
        if ([keyPath isEqualToString:@"status"]) {
            switch (self.player.status) {
                    case AVPlayerStatusReadyToPlay:
                {
                    //准备播放完成了
                    //音乐的总时间
                    self.data.duration= CMTimeGetSeconds(self.player.currentItem.asset.duration);
                    
                    LogDebugTag(MusicPlayerManagerTag, @"observeValue status ReadyToPlay duration:%f",self.data.duration);
                                    
                    //回调代理
                    if (self.delegate) {
                        [self.delegate onPrepared:_data];
                    }
                    
                    //更新媒体控制中心信息
                    [self updateMediaInfo];
                    
                }
                    break;
                    case AVPlayerStatusFailed:
                {
                    //播放失败了
                    _status = PlayStatusError;
                    
                    LogDebugTag(MusicPlayerManagerTag, @"observeValue status play error");
                }
                    break;
                    
                default:{
                    //未知状态
                    LogDebugTag(MusicPlayerManagerTag, @"observeValue status unknown");
                    _status = PlayStatusNone;
                }
                    break;
            }
            
        }
        ...
    }
    
    
    - (void)startPublishProgress{
        //判断是否启动了
        if (_playTimeObserve) {
            //已经启动了
            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);
            
            //当前播放的时间
            self.data.progress = CMTimeGetSeconds(time);
            
            //判断是否有代理
            if (!self.delegate) {
                //没有回调
                //停止定时器
                [self stopPublishProgress];
                return;
            }
            
            //回调代理
            [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{
        //更改状态
        _status = PlayStatusPause;
        
        //暂停
        [self.player pause];
        
        //移除监听器
        [self removeListeners];
    
        //回调代理
        if (self.delegate) {
            [self.delegate onPaused:_data];
        }
    
        //停止进度分发定时器
        [self stopPublishProgress];
    }
    
    - (void)resume{
        //设置音频会话
        [SuperAudioSessionManager requestAudioFocus];
        
        //更改播放状态
        _status = PlayStatusPlaying;
        
        //播放
        [self.player play];
        
        ...
    }
    
    - (void)seekTo:(float)data{
        [self.player seekToTime:CMTimeMake(data, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    }
    
    #pragma mark - 媒体中心
    
    /// 更新系统媒体控制中心信息
    /// 不需要更新进度到控制中心
    /// 他那边会自动倒计时
    /// 这部分可以重构到公共类,因为像播放视频也可以更新到系统媒体中心
    -(void)updateMediaInfo{
        //下载图片,这部分应该封装,因为其他界面也用到了
        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) {
            //进度,这里用不到
        } 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;
        }];
    
        //设置封面
        [songInfo setObject:albumArt forKey:MPMediaItemPropertyArtwork];
    
        ...
    
        //设置到系统
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];
    }
    
    - (void)setDelegate:(id<MusicPlayerManagerDelegate>)delegate{
        _delegate = delegate;
        if (_delegate) {
            //有代理
            
            //判断是否有音乐在播放
            if ([self isPlaying]) {
                //有音乐在播放
                
                //启动定时器
                [self startPublishProgress];
            }
        } else {
            //没有代理
            
            //停止定时器
            [self stopPublishProgress];
        }
    }
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318

    音乐列表逻辑封装到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;
            
            //设置播放完毕回调
            [self.musicPlayerManager setComplete:^(Song * _Nonnull data) {
                
                //判断播放循环模式
                if ([weakSelf getLoopModel] == MusicPlayRepeatModelOne) {
                    //单曲循环
                    [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;
    }
    
    /// 设置默认播放音乐
    -(void)defaultPlaySong{
        _data=_datum[0];
    }
    
    /// 设置播放列表
    - (void)setDatum:(NSArray *)datum{
        //将原来数据playList标志设置为false
        [DataUtil changePlayListFlag:_datum inList:NO];
    
        //保存到数据库
        [self saveAll];
    
        //清空原来的数据
        [_datum removeAllObjects];
    
        //添加新的数据
        [_datum addObjectsFromArray:datum];
    
        //更改播放列表标志
        [DataUtil changePlayListFlag:_datum inList:YES];
    
        //保存到数据库
        [self saveAll];
    
        [self sendMusicListChanged];
    }
    
    /// 保存当前播放列表到数据库
    -(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;
        
        //标记为播放了
        self.isPlay = YES;
        
        NSString *path;
        
        //查询是否有下载任务
        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];
        
        //设置最后播放音乐的Id
        [PreferenceUtil setLastPlaySongId:_data.id];
    }
    
    /**
     * 暂停
     */
    - (void)pause{
        LogDebugTag(MusicListManagerTag, @"pause");
        [_musicPlayerManager pause];
    }
    
    ...
    
    /// 更改循环模式
    - (MusicPlayRepeatModel)changeLoopModel{
        //循环模式+1
        _model++;
    
        //判断循环模式边界
        if (_model > MusicPlayRepeatModelRandom) {
            //如果当前循环模式
            //大于最后一个循环模式
            //就设置为第0个循环模式
            _model = MusicPlayRepeatModelList;
        }
        
        //返回最终的循环模式
        return _model;
    }
    
    /**
     * 获取循环模式
     */
    - (MusicPlayRepeatModel)getLoopModel{
        return _model;
    }
    
    - (Song *)getData{
        return self.data;
    }
    
    /**
     * 获取上一个
     */
    - (Song *)previous{
        //音乐索引
        NSUInteger index = 0;
    
        //判断循环模式
        switch (self.model) {
            case MusicPlayRepeatModelRandom:{
                //随机循环
    
                //在0~datum.size()中
                //不包含datum.size()
                index = arc4random() % [_datum count];
            }
                break;
            default:{
                //找到当前音乐索引
                index = [_datum indexOfObject:self.data];
    
                if (index != -1) {
                    //找到了
    
                    //如果当前播放是列表第一个
                    if (index == 0) {
                        //第一首音乐
    
                        //那就从最后开始播放
                        index = [_datum count] - 1;
                    } else {
                        index--;
                    }
                } else {
                    //抛出异常
                    //因为正常情况下是能找到的
                    
                }
            }
                break;
        }
    
        //获取音乐
        return [_datum objectAtIndex:index];
    }
    
    ...
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206

    外界统一使用播放列表管理器播放音乐,上一曲下一曲:

    -(void)onLoopModelClick:(UIButton *)sender{
        //更改循环模式
        [[MusicListManager shared] changeLoopModel];
    
        //显示循环模式
        [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]];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    歌词

    歌词实现了LRC,KSC两种歌词,封装到LyricListView,单个歌词行封装到LyricView中,外界直接使用LyricListView就行:

    /// 显示歌词数据
    -(void)showLyricData{
        _lyricView.data = [[MusicListManager shared] getData].parsedLyric;
    }
    
    • 1
    • 2
    • 3
    • 4

    歌词控件封装:

    @implementation LyricListView
    
    - (instancetype)init{
        self=[super init];
        
        self.datum = [NSMutableArray array];
        
        [self initViews];
        
        return self;
    }
    
    - (void)initViews{
        //设置约束
        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];
        
        //创建一个水平方向容器
        _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) {
            //已经计算了填充数量
            [self next];
        }
    }
    
    - (void)next{
        //清空原来的歌词
        [_datum removeAllObjects];
    
        if (_data) {
            //添加占位数据
            [self addLyricFillData];
            [_datum addObjectsFromArray:_data.datum];
    
            //添加占位数据
            [self addLyricFillData];
        }
    
        _isReloadData=YES;
        [_tableView reloadData];
    }
    
    /// 添加歌词占位数据
    /// 添加的目的是让第一行歌词也能显示到控件垂直方向中心
    -(void)addLyricFillData {
        for (int i=0; i<_lyricPlaceholderSize; i++) {
            [_datum addObject:@"fill"];
        }
    }
    
    - (void)setProgress:(float)progress{
        if(!_isReloadData && _lyricPlaceholderSize > 0){
            //还没有加载数据
            
            //所以这里加载数据
            [self next];
        }
        
        if (_data && _datum.count>0) {
            if (_isDrag) {
               //正在拖拽歌词
               //就直接返回
               return;
            }
            
            //获取当前时间对应的歌词索引
            NSInteger newLineNumber = [LyricUtil getLineNumber:_data progress:progress] + _lyricPlaceholderSize;
    
            if (newLineNumber != _lyricLineNumber) {
               //滚动到当前行
               [self scrollPosition:newLineNumber];
    
               _lyricLineNumber = newLineNumber;
            }
            
            //如果是精确到字歌曲
           //还需要将时间分发到item中
           //因为要持续绘制
           if (_data.isAccurate) {
               NSObject *object = _datum[_lyricLineNumber];
               if ([object isKindOfClass:[LyricLine class]]) {
                   //只有是歌词行才处理
    
                   //获取当前时间是该行的第几个字
                   NSInteger lyricCurrentWordIndex=[LyricUtil getWordIndex:object progress:progress];
    
                   //获取当前时间改字
                   //已经播放的时间
                   NSInteger wordPlayedTime=[LyricUtil getWordPlayedTime:object progress:progress];
    
                   //获取cell
                   LyricCell *cell= [self getCell:self.lyricLineNumber];
    
                   if (cell) {
                       //有可能获取不到当前位置的Cell
                       //因为上面使用了滚动动画
                       //如果不使用滚动动画效果不太好
    
                       //将当前时间对应的字索引设置到控件
                       [cell.lineView setLyricCurrentWordIndex:lyricCurrentWordIndex];
    
                       //设置当前字已经播放的时间
                       [cell.lineView setWordPlayedTime:wordPlayedTime];
    
                       //标记需要绘制
                       [cell.lineView setNeedsDisplay];
                   }
    
               }
           }
        }
    }
    
    ...
    
    #pragma mark - 列表数据源
    /// 有多少个
    /// @param tableView <#tableView description#>
    /// @param section <#section description#>
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
        return _datum.count;
    }
    
    /// 返回当前位置的cell
    /// @param tableView <#tableView description#>
    /// @param indexPath <#indexPath description#>
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
        //获取cell
        LyricCell *cell=[tableView dequeueReusableCellWithIdentifier:Cell forIndexPath:indexPath];
        
        //设置Tag
        cell.tag = indexPath.row;
        
        //取出数据
        NSObject *data = _datum[indexPath.row];
        
        //绑定数据
        [cell bind:data accurate:_data.isAccurate];
        
        //返回cell
        return cell;
    }
    
    #pragma mark - 滚动相关
    
    /// 开始拖拽时调用
    /// @param scrollView <#scrollView description#>
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
        LogDebugTag(LyricListViewTag, @"scrollViewWillBeginDragging");
        [self showDragView];
    }
    
    /// 拖拽结束
    /// @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];
        }
    }
    
    /// 滚动结束(惯性滚动)
    /// @param scrollView <#scrollView description#>
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
        NSLog(@"lyric view scrollViewDidEndDecelerating");
        //如果需要减速,在这里延时后,显示歌词
        [self prepareScrollLyricView];
    }
    
    ...
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208

    控制器

    使用了可以通过系统媒体控制器,通知栏,锁屏界面,耳机,蓝牙耳机等设备控制媒体播放暂停,只需要把媒体信息更新到系统:

    - (void)setMediaInfo:(UIImage *)image{
        //初始化一个可变字典
        NSMutableDictionary *songInfo=[[NSMutableDictionary alloc] init];
    
        //初始化一个封面
        MPMediaItemArtwork *albumArt=[[MPMediaItemArtwork alloc] initWithBoundsSize:image.size requestHandler:^UIImage * _Nonnull(CGSize size) {
            return image;
        }];
    
        //设置封面
        [songInfo setObject:albumArt forKey:MPMediaItemPropertyArtwork];
    
        //歌曲名称
        [songInfo setObject:self.data.title forKey:MPMediaItemPropertyTitle];
    
        ...
    
        //设置到系统
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    媒体控制

    /// 接收远程音乐播放控制消息
    /// 例如:点击耳机上的按钮,点击媒体控制中心按钮等
    /// @param event <#event description#>
    - (void)remoteControlReceivedWithEvent:(UIEvent *)event{
        //判断是不是远程控制事件
        if (event.type == UIEventTypeRemoteControl) {
            if ([[MusicListManager shared] getData] == nil) {
                //当前播放列表中没有音乐
                return;
            }
    
            //判断事件类型
            switch (event.subtype) {
                case UIEventSubtypeRemoteControlPlay:{
                    //点击了播放按钮
                    [[MusicListManager shared] resume];
                    NSLog(@"AppDelegate play");
                }
                    break;
                case UIEventSubtypeRemoteControlPause:{
                    //点击了暂停
                    [[MusicListManager shared] pause];
                    NSLog(@"AppDelegate pause");
                }
                    break;
                case UIEventSubtypeRemoteControlNextTrack:{
                    //下一首
                    //双击iPhone有线耳机上的控制按钮
                    Song *song = [[MusicListManager shared] next];
                    [[MusicListManager shared] play:song];
                    NSLog(@"AppDelegate Next");
                }
                    break;
                ...
                default:
                    break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    登录/注册/验证码登录

    登录注册没有多大难度,用户名和密码登录,就是把信息传递到服务端,可以加密后在传输,服务端判断登录成功,返回一个标记,客户端保存,其他需要的登录的接口带上;验证码登录就是用验证码代替密码,发送验证码都是服务端发送,客户端只需要调用接口。

    评论

    在这里插入图片描述
    评论列表包括下拉刷新,上拉加载更多,点赞,发布评论,回复评论,Emoji,话题和提醒人点击,选择好友,选择话题等。

    刷新和下拉加载更多

    核心逻辑就只需要更改page就行了

    //下拉刷新
    MJRefreshNormalHeader *header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{
        @strongify(self);
        [self loadData];
    }];
    
    //隐藏标题
    header.stateLabel.hidden = YES;
    
    // 隐藏时间
    header.lastUpdatedTimeLabel.hidden = YES;
    self.tableView.mj_header=header;
    
    //上拉加载更多
    MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
        @strongify(self);
        [self loadMore];
    }];
    
    // 设置空闲时文字
    [footer setTitle:@"" forState:MJRefreshStateIdle];
    
    self.tableView.mj_footer = footer;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    人和话题点击

    通过正则表达式,找到特殊文本,然后使用富文本实现点击。

    /// 处理文本点击事件
    /// 这部分可以用监听器回调到界面处理
    /// @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);
            }
        }];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    好友

    @implementation UserController
    
    - (void)initViews{
        [super initViews];
        
        //初始化TableView结构
        [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 - 列表数据源
    
    /// 返回当前位置的cell
    /// 相当于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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    视频和播放

    在这里插入图片描述

    真实项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包括审核,转码,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];
    //
    //    //设置标题
    //    [self.playerView.controlView setTitle:data.title];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    用户详情/更改资料

    在这里插入图片描述

    用户详情顶部显示用户信息,好友数量,下面分别显示创建的歌单,收藏的歌单,发布的动态,类似微信朋友圈,右上角可以更改用户资料;使用第三方框架里面的kJXPagingListRefreshView控件实现。

    -(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)];
        
        //标题
        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];
        
        //默认颜色
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    然后就是把每个子界面放到单独View中,并在代理方法返回就行了。

    发布动态/选择位置/路径规划

    在这里插入图片描述
    发布效果和微信朋友圈类似,可以选择图片,和地理位置;地理位置使用高德地图实现选择,路径规划是调用系统中安装的地图,类似微信。

    位置

    /// 搜索该位置的poi,方便用户选择,也方便其他人找
    -(void)searchPOI{
    	//LogDebug(@"searchPOI %f %f %@",data.);
        if (_keyword) {
            //关键字搜索
            AMapPOIKeywordsSearchRequest *request = [AMapPOIKeywordsSearchRequest new];
            
            //关键字
            request.keywords=_keyword;
    
            //距离排序
            request.sortrule = 0;
    
            //是否返回扩展信息
            request.requireExtension=YES;
    
            [self.search AMapPOIKeywordsSearch:request];
            
        } else {
            //搜索位置附近
            AMapPOIAroundSearchRequest *request = [AMapPOIAroundSearchRequest new];
            request.location=[AMapGeoPoint locationWithLatitude:_coordinate.latitude longitude:_coordinate.longitude];
            
            //距离排序
            request.sortrule=0;
            
            //是否返回扩展信息
            request.requireExtension=YES;
            
            [self.search AMapPOIAroundSearch:request];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    地图路径规划

    + (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];
    }
    
    • 1
    • 2
    • 3
    • 4

    聊天/离线推送

    在这里插入图片描述
    大部分真实项目中聊天都会选择第三方商业级付费聊天服务,常用的有腾讯云聊天,融云聊天,网易云聊天等,这里选择融云聊天服务,使用步骤是先在服务端生成聊天Token,这里是登录后返回,然后客户端登录聊天服务器,然后设置消息监听,发送消息等。

    聊天服务器

    /// 连接聊天服务器
    /// @param data <#data description#>
    -(void)connectChat:(Session *)data{
        [[RCIMClient sharedRCIMClient] connectWithToken:data.chatToken dbOpened:^(RCDBErrorCode code) {
                    //消息数据库打开,可以进入到主页面
                } success:^(NSString *userId) {
                    //连接成功
                } error:^(RCConnectErrorCode status) {
                    if (status == RC_CONN_TOKEN_INCORRECT) {
                        //从 APP 服务获取新 token,并重连
                    } else {
                        //无法连接到 IM 服务器,请根据相应的错误码作出对应处理
                    }
    
                    //因为我们这个应用,不是类似微信那样纯聊天应用,所以聊天服务器连接失败,也让进入应用
                    //真实项目中按照需求实现就行了
                    [SuperToast showWithTitle:R.string.localizable.errorMessageLogin];
                }];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    消息监听

    - (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];
            }
            
            //发送消息到通知(这个通知是,跨界面通讯,不是显示到通知栏)
            [NSNotificationCenter.defaultCenter postNotificationName:ON_MESSAGE object:nil userInfo:@{@"data":message}];
            
            //发送消息未读数改变了通知
            [NSNotificationCenter.defaultCenter postNotificationName:ON_MESSAGE_COUNT_CHANGED object:nil userInfo:nil];
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    文本消息

    发送图片等其他消息也是差不多。

    /// 发送文本消息
    -(void)sendTextMessage{
        NSString *result=_contentInputView.text;
        
        if([StringUtil isBlank:result]){
            [SuperToast showWithTitle:R.string.localizable.hintEnterMessage];
            return;
        }
        
        //1.构造文本消息
        RCTextMessage *txtMsg = [RCTextMessage messageWithContent:result];
    
        //2.将文本消息发送出去
        [[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(@"消息发送成功,message id 为 %@",@(messageId));
    
            dispatch_async(dispatch_get_main_queue(), ^{
                //清空输入框
                [self clearInput];
            });
    
            [self addMessage:[[RCIMClient sharedRCIMClient] getMessage:messageId]];
    
        } error:^(RCErrorCode nErrorCode, long messageId) {
    
            NSLog(@"消息发送失败,错误码 为 %@",@(nErrorCode));
            
        }];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    离线推送

    需要付费苹果开发者账户,先开启SDK离线推送,然后在苹果开发者后台创建推送证书,配置到融云,最后在代码中处理通知点击等。

    /// 界面已经显示了
    /// @param animated <#animated description#>
    - (void)viewDidAppear:(BOOL)animated{
        [super viewDidAppear:animated];
    
        //延时的目的是让当前界面显示出来以后,在检查
        //检查是否需要处理通知点击
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
            //检查是否需要处理通知点击
            [self checkProcessNotificationClick];
         });
    }
    
    /// 检查是否需要处理通知点击
    -(void)checkProcessNotificationClick{
        if ([AppDelegate shared].pushData) {
            [self processPushClick:[AppDelegate shared].pushData];
    
            [AppDelegate shared].pushData=nil;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    商城/订单/支付/购物车

    在这里插入图片描述
    在这里插入图片描述

    学到这里,大家不能说熟悉,那么看到上面的界面,那么大体要能实现出来。

    详情富文本

    //详情
    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];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    宝/微信支付

    客户端先集成微信,支付宝SDK,然后请求服务端获取支付信息,设置到SDK,最后就是处理支付结果。

    /// 处理支付宝支付
    /// @param data <#data description#>
    - (void)processAlipay:(NSString *)data{
        //支付宝官方开发文档:https://docs.open.alipay.com/204/105295/
        [[AlipaySDK defaultService] payOrder:data fromScheme:ALIPAY_CALLBACK_SCHEME callback:^(NSDictionary *resultDic) {
            //如果手机中没有安装支付宝客户端
            //会跳转H5支付页面
            //支付相关的信息会通过这个方法回调
    
            //处理支付宝支付结果
            [self processAlipayResult:resultDic];
        }];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    支付结果

    /// 处理支付宝支付结果
    /// @param data <#data description#>
    - (void)processAlipayResult:(NSDictionary *)data{
        NSString *resultStatus=data[@"resultStatus"];
    
        if ([@"9000" isEqualToString:resultStatus]) {
            //本地支付成功
    
            //不能依赖本地支付结果
            //一定要以服务端为准
            [SuperToast showLoading:R.string.localizable.hintPayWait];
    
            [self checkPayStatus];
            
            //这里就不根据服务端判断了
            //购买成功统计
            [AnalysisUtil onPurchase:YES data:self.data];
        }if ([@"6001" isEqualToString:resultStatus]) {
            //取消了
            [self showCancel];
        } else {
            //支付失败
            [self showPayFailedTip];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    项目总结

    总体来说项目功能还是很全的,还有一些小功能,例如:快捷方式等就不在贴代码了,但肯定没发和原版比,相信大家只要做过程序员就能理解,毕竟原版是一个商业级项目,几十个人天天开发和维护,而且持续了几年了;不过恕我直言,现在的常见的音乐软件都太复杂了,各种功能,不过都要恰饭,好像又能理解了😄。

  • 相关阅读:
    【博客474】为什么k8s控制面pod使用的ip是node ip,而非pod cidr中的ip
    Error could not open `Ejdklibamd64jvm.cfg‘问题解决
    心肺运动试验----各类参数分析笔记
    Android系统稳定性简述
    掌动智能:功能测试及拨测主要功能
    SiC,GaN驱动优选驱动方案SiLM5350系列SiLM5350MDDCM-DG 带米勒钳位Clamp保护功能 单通道隔离栅极驱动器
    字节的一个小问题 npm 和 yarn不一样吗?
    【21天学习经典算法】插入排序(附Python完整代码)
    MySQL 8.0与MySQL 5.7的binlog差异小结
    猿创征文 |【Linux】常用命令
  • 原文地址:https://blog.csdn.net/renpingqing/article/details/126268364