Swift仿写喜马拉雅FM

栏目: Swift · 发布时间: 6年前

内容简介:该项目采用与项目首页推荐模块,根据接口请求数据进行处理,顶部的
  • 最近抽空面了几家公司,大部分都是从基础开始慢慢深入项目和原理。面试内容还是以 OC 为主,但是多数也都会问一下 Swift 技术情况,也有例外全程问 Swift 的公司(做区块链项目),感觉现在虽然大多数公司任然以 OC 做为主开发语言,但是 Swift 发展很强势,估计明年 Swift5 以后使用会更加广泛。
  • 另外,如果准备跳槽的话,可以提前投简历抽空面试几家公司,一方面可以通过投递反馈检验简历,另外可以总结面试的大致问题方向有利于做针对性复习,毕竟会用也要会说才行,会说也要能说到重点才行,还有就是心仪的公司一定要留到最后面试。希望都能进一个心仪不坑的公司,当然也应努力提升自己的技术,不坑公司不坑团队, 好像跑题了!!!

目录:

关于项目:

该项目采用 MVC + MVVM 设计模式, Moya + SwiftyJSON + HandyJSON 网络框架和数据解析。数据来源抓包及部分本地 json 文件。 使用 Xcode9.4 基于 Swift4.1 进行开发。 项目中使用到的一些开源库以下列表,在这里感谢作者的开源。

pod 'SnapKit'
    pod 'Kingfisher'
    #tabbar样式
    pod 'ESTabBarController-swift'
    #banner滚动图片
    pod 'FSPagerView'
    pod 'Moya'
    pod 'HandyJSON'
    pod 'SwiftyJSON'
    # 分页
    pod 'DNSPageView'
    #跑马灯
    pod 'JXMarqueeView'
    #滚动页
    pod 'LTScrollView'
    #刷新
    pod 'MJRefresh'
    #消息提示
    pod 'SwiftMessages'
    pod 'SVProgressHUD'
    #播放网络音频
    pod 'StreamingKit'
复制代码

效果图:

Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
项目按照 MVVM 模式进行设计,下面贴一下 ViewModel 中接口请求和布局设置方法代码。
import UIKit
import SwiftyJSON
import HandyJSON
class HomeRecommendViewModel: NSObject {
    // MARK - 数据模型
     var fmhomeRecommendModel:FMHomeRecommendModel?
     var homeRecommendList:[HomeRecommendModel]?
     var recommendList : [RecommendListModel]?
    // Mark: -数据源更新
    typealias AddDataBlock = () ->Void
    var updataBlock:AddDataBlock?

// Mark:-请求数据
extension HomeRecommendViewModel {
    func refreshDataSource() {
        //首页推荐接口请求
        FMRecommendProvider.request(.recommendList) { result in
            if case let .success(response) = result {
                //解析数据
                let data = try? response.mapJSON()
                let json = JSON(data!)
                if let mappedObject = JSONDeserializer<FMHomeRecommendModel>.deserializeFrom(json: json.description) { // 从字符串转换为对象实例
                    self.fmhomeRecommendModel = mappedObject
                    self.homeRecommendList = mappedObject.list
                    if let recommendList = JSONDeserializer<RecommendListModel>.deserializeModelArrayFrom(json: json["list"].description) {
                        self.recommendList = recommendList as? [RecommendListModel]
                    }
            }
      }
}

// Mark:-collectionview数据
extension HomeRecommendViewModel {
    func numberOfSections(collectionView:UICollectionView) ->Int {
        return (self.homeRecommendList?.count) ?? 0
    }
    // 每个分区显示item数量
    func numberOfItemsIn(section: NSInteger) -> NSInteger {
        return 1
    }
    //每个分区的内边距
    func insetForSectionAt(section: Int) -> UIEdgeInsets {
        return UIEdgeInsetsMake(0, 0, 0, 0)
    }
    //最小 item 间距
    func minimumInteritemSpacingForSectionAt(section:Int) ->CGFloat {
        return 0
    }
    //最小行间距
    func minimumLineSpacingForSectionAt(section:Int) ->CGFloat {
        return 0
    }
   // 分区头视图size
    func referenceSizeForHeaderInSection(section: Int) -> CGSize {
        let moduleType = self.homeRecommendList?[section].moduleType
        if moduleType == "focus" || moduleType == "square" || moduleType == "topBuzz" || moduleType == "ad" || section == 18 {
            return CGSize.zero
        }else {
            return CGSize.init(width: YYScreenHeigth, height:40)
        }
    }
    
    // 分区尾视图size
    func referenceSizeForFooterInSection(section: Int) -> CGSize {
        let moduleType = self.homeRecommendList?[section].moduleType
        if moduleType == "focus" || moduleType == "square" {
            return CGSize.zero
        }else {
            return CGSize.init(width: YYScreenWidth, height: 10.0)
        }
    }
}
复制代码

ViewModel 相对应的是控制器 Controller.m 文件中的使用,使用 MVVM 可以梳理 Controller 看起来更整洁一点,避免满眼的逻辑判断。

lazy var viewModel: HomeRecommendViewModel = {
        return HomeRecommendViewModel()
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.collectionView)
        self.collectionView.snp.makeConstraints { (make) in
            make.width.height.equalToSuperview()
            make.center.equalToSuperview()
        }
        self.collectionView.uHead.beginRefreshing()
        loadData()
        loadRecommendAdData()
    }
    func loadData(){
        // 加载数据
        viewModel.updataBlock = { [unowned self] in
            self.collectionView.uHead.endRefreshing()
            // 更新列表数据
            self.collectionView.reloadData()
        }
        viewModel.refreshDataSource()
    }

// MARK - collectionDelegate
extension HomeRecommendController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return viewModel.numberOfSections(collectionView:collectionView)
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel.numberOfItemsIn(section: section)
    }
      //每个分区的内边距
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return viewModel.insetForSectionAt(section: section)
    }

    //最小 item 间距
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return viewModel.minimumInteritemSpacingForSectionAt(section: section)
    }

    //最小行间距
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return viewModel.minimumLineSpacingForSectionAt(section: section)
    }
    
    //item 的尺寸
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
       return viewModel.sizeForItemAt(indexPath: indexPath)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        return viewModel.referenceSizeForHeaderInSection(section: section)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
       return viewModel.referenceSizeForFooterInSection(section: section)
    }
复制代码

首页模块分析:

项目首页推荐模块,根据接口请求数据进行处理,顶部的 Banner 滚动图片和分类按钮以及下面的听头条统一划分为 HeaderCell ,在这个 HeaderCell 中继续划分,顶部 Banner 单独处理,下面创建 CollectionView ,并把分类按钮和听头条作为两个 Section ,其中听头条的实现思路为 CollectionCell ,通过定时器控制器自动上下滚动。

Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM
首页推荐的其他模块根据接口请求得到的 moduleType 进行 Section 初始化并返回不同样式的 Cell ,另外在该模块中还穿插有广告,广告为单独接口,根据接口返回数据穿插到对应的 Section
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let moduleType = viewModel.homeRecommendList?[indexPath.section].moduleType
        if moduleType == "focus" || moduleType == "square" || moduleType == "topBuzz" {
                let cell:FMRecommendHeaderCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendHeaderCellID, for: indexPath) as! FMRecommendHeaderCell
                cell.focusModel = viewModel.focus
                cell.squareList = viewModel.squareList
                cell.topBuzzListData = viewModel.topBuzzList
                cell.delegate = self
                return cell
        }else if moduleType == "guessYouLike" || moduleType == "paidCategory" || moduleType == "categoriesForLong" || moduleType == "cityCategory"{
            ///横式排列布局cell
                let cell:FMRecommendGuessLikeCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendGuessLikeCellID, for: indexPath) as! FMRecommendGuessLikeCell
                cell.delegate = self
                cell.recommendListData = viewModel.homeRecommendList?[indexPath.section].list
                return cell
        }else if moduleType == "categoriesForShort" || moduleType == "playlist" || moduleType == "categoriesForExplore"{
            // 竖式排列布局cell
                let cell:FMHotAudiobookCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMHotAudiobookCellID, for: indexPath) as! FMHotAudiobookCell
            cell.delegate = self
                cell.recommendListData = viewModel.homeRecommendList?[indexPath.section].list
                return cell
        }else if moduleType == "ad" {
                let cell:FMAdvertCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMAdvertCellID, for: indexPath) as! FMAdvertCell
            if indexPath.section == 7 {
                cell.adModel = self.recommnedAdvertList?[0]
            }else if indexPath.section == 13 {
                cell.adModel = self.recommnedAdvertList?[1]
            }
                return cell
        }else if moduleType == "oneKeyListen" {
                let cell:FMOneKeyListenCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMOneKeyListenCellID, for: indexPath) as! FMOneKeyListenCell
            cell.oneKeyListenList = viewModel.oneKeyListenList
                return cell
        }else if moduleType == "live" {
            let cell:HomeRecommendLiveCell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeRecommendLiveCellID, for: indexPath) as! HomeRecommendLiveCell
            cell.liveList = viewModel.liveList
            return cell
        }
        else {
                let cell:FMRecommendForYouCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendForYouCellID, for: indexPath) as! FMRecommendForYouCell
                return cell

        }

    }
复制代码

项目中分区尺寸高度是根据返回数据的 Count 进行计算的,其他各模块基本思路相同这里只贴一下首页模块分区的尺寸高度计算。

// item 尺寸
    func sizeForItemAt(indexPath: IndexPath) -> CGSize {
        let HeaderAndFooterHeight:Int = 90
        let itemNums = (self.homeRecommendList?[indexPath.section].list?.count)!/3
        let count = self.homeRecommendList?[indexPath.section].list?.count
        let moduleType = self.homeRecommendList?[indexPath.section].moduleType
        if moduleType == "focus" {
            return CGSize.init(width:YYScreenWidth,height:360)
        }else if moduleType == "square" || moduleType == "topBuzz" {
            return CGSize.zero
        }else if moduleType == "guessYouLike" || moduleType == "paidCategory" || moduleType == "categoriesForLong" || moduleType == "cityCategory" || moduleType == "live"{
            return CGSize.init(width:YYScreenWidth,height:CGFloat(HeaderAndFooterHeight+180*itemNums))
        }else if moduleType == "categoriesForShort" || moduleType == "playlist" || moduleType == "categoriesForExplore"{
            return CGSize.init(width:YYScreenWidth,height:CGFloat(HeaderAndFooterHeight+120*count!))
        }else if moduleType == "ad" {
            return CGSize.init(width:YYScreenWidth,height:240)
        }else if moduleType == "oneKeyListen" {
            return CGSize.init(width:YYScreenWidth,height:180)
        }else {
            return .zero
        }
    }
复制代码

首页分类模块分析:

首页分类采用的是 CollectionView 展示分类列表,点击每个分类 Item 进入对应的分类界面,根据 categoryId 请求顶部滚动 title 数据,另外该数据不包含推荐模块,所以分类整体为两个 Controller ,一个为推荐模块,一个为其他分类界面根据不同 categoryId 显示不同数据列表(因为该界面数据样式一样都是列表),然后推荐部分按照首页的同等思路根据不同的 moduleType 显示不同类型 Cell

Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM

首页Vip模块分析:

首页 Vip 模块与推荐模块较为相似,顶部 Banner 滚动图片和分类按钮作为顶部 Cell ,然后其他 Cell 横向显示或者是竖向显示以及显示的 Item 数量根据接口而定,分区的标题同样来自于接口数据,点击分区 headerVeiw 的更多按钮跳转到该分区模块的更多页面。

Swift仿写喜马拉雅FM

首页直播模块分析:

首页直播界面的排版主要分四个部分也就是自定义四个 CollectionCell ,顶部分类按钮,接着是 Banner 滚动图片 Cell 内部使用 FSPagerView 实现滚动图片效果,滚动排行榜为 Cell 内部嵌套 CollectionView ,通过定时器控制 CollectionCell 实现自动滚动,接下来就是播放列表了,通过自定义 HeaderView 上面的按钮切换,刷新不同类型的播放列表。

Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM

首页广播模块分析:

首页广播模块主要分三个部分,顶部分类按钮 Cell ,中间可展开收起分类 Item ,因为接口中返回的是 14 个电台分类,收起状态显示 7 个电台和展开按钮,展开状态显示 14 个电台和收起按钮中间空一格 Item ,在 ViewModel 中获取到数据之后进行插入图片按钮并根据当前展开或是收起状态返回不同 Item 数据来实现这部分功能,剩下的是根据数据接口中的分区显示列表和 HeaderView 内容。

点击广播顶部分类 Item 跳转到对应界面,但是接口返回的该 Item 参数为 Url 中拼接的字段例如:url:"iting://open?msg_type=70&api=http://live.ximalaya.com/live-web/v2/radio/national&title=国家台&type=national",所以我们要解析 Url 拼接参数为字典,拿到我们所需的跳转下一界面请求接口用到的字段。下面为代码部分:

func getUrlAPI(url:String) -> String {
       // 判断是否有参数
       if !url.contains("?") {
           return ""
       }
       var params = [String: Any]()
       // 截取参数
       let split = url.split(separator: "?")
       let string = split[1]
       // 判断参数是单个参数还是多个参数
       if string.contains("&") {
           // 多个参数,分割参数
           let urlComponents = string.split(separator: "&")
           // 遍历参数
           for keyValuePair in urlComponents {
               // 生成Key/Value
               let pairComponents = keyValuePair.split(separator: "=")
               let key:String = String(pairComponents[0])
               let value:String = String(pairComponents[1])
               
               params[key] = value
           }
       } else {
           // 单个参数
           let pairComponents = string.split(separator: "=")
           // 判断是否有值
           if pairComponents.count == 1 {
               return "nil"
           }
           let key:String = String(pairComponents[0])
           let value:String = String(pairComponents[1])
           params[key] = value as AnyObject
       }
       guard let api = params["api"] else{return ""}
       return api as! String
   }
复制代码
Swift仿写喜马拉雅FM

我听模块分析:

我听模块主页面顶部为自定义 HeaderView ,内部循环创建按钮,下面为使用 LTScrollView 管理三个子模块的滚动视图,订阅和推荐为固定列表显示接口数据,一键听模块也是现实列表数据,其中有个跑马灯滚动显示重要内容的效果,点击添加频道,跳转更多频道界面,该界面为双 TableView 实现联动效果,点击左边分类 LeftTableView 对应右边 RightTableView 滚动到指定分区,滚动右边 RightTableView 对应的左边 LeftTableView 滚动到对应分类。

Swift仿写喜马拉雅FM

发现模块分析:

发现模块主页面顶部为自定义 HeaderView ,内部嵌套 CollectionView 创建分类按钮 Item ,下面为使用 LTScrollView 管理三个子模块的滚动视图,关注和推荐动态类似都是显示图片加文字形式显示动态,这里需要注意的是根据文字内容和图片的张数计算当前 Cell 的高度,趣配音就是正常的列表显示。

下面贴一个计算动态发布距当前时间的代码
复制代码
//MARK: -根据后台时间戳返回几分钟前,几小时前,几天前
    func updateTimeToCurrennTime(timeStamp: Double) -> String {
        //获取当前的时间戳
        let currentTime = Date().timeIntervalSince1970
        //时间戳为毫秒级要 / 1000, 秒就不用除1000,参数带没带000
        let timeSta:TimeInterval = TimeInterval(timeStamp / 1000)
        //时间差
        let reduceTime : TimeInterval = currentTime - timeSta
        //时间差小于60秒
        if reduceTime < 60 {
            return "刚刚"
        }
        //时间差大于一分钟小于60分钟内
        let mins = Int(reduceTime / 60)
        if mins < 60 {
            return "\(mins)分钟前"
        }
        //时间差大于一小时小于24小时内
        let hours = Int(reduceTime / 3600)
        if hours < 24 {
            return "\(hours)小时前"
        }
        //时间差大于一天小于30天内
        let days = Int(reduceTime / 3600 / 24)
        if days < 30 {
            return "\(days)天前"
        }
        //不满足上述条件---或者是未来日期-----直接返回日期
        let date = NSDate(timeIntervalSince1970: timeSta)
        let dfmatter = DateFormatter()
        //yyyy-MM-dd HH:mm:ss
        dfmatter.dateFormat="yyyy年MM月dd日 HH:mm:ss"
        return dfmatter.string(from: date as Date)
    }
复制代码
Swift仿写喜马拉雅FM

我的模块分析:

我的界面在这里被划分为了三个模块,顶部的头像、名称、粉丝等一类个人信息作为 TableViewHeaderView ,并且在该 HeaderView 中循环创建了已购、优惠券等按钮,然后是 Section0 循环创建录音、直播等按钮,下面的 Cell 根据 dataSource 进行分区显示及每个分区的 count 。在我的界面中使用了两个小动画,一个是上下滚动的优惠券引导领取动画,另一个是我要录音一个波状扩散提示录音动画。

下面贴一下波纹扩散动画的代码
复制代码
import UIKit

class CVLayerView: UIView {
    var pulseLayer : CAShapeLayer!  //定义图层
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        let width = self.bounds.size.width
        
        // 动画图层
        pulseLayer = CAShapeLayer()
        pulseLayer.bounds = CGRect(x: 0, y: 0, width: width, height: width)
        pulseLayer.position = CGPoint(x: width/2, y: width/2)
        pulseLayer.backgroundColor = UIColor.clear.cgColor
        // 用BezierPath画一个原型
        pulseLayer.path = UIBezierPath(ovalIn: pulseLayer.bounds).cgPath
        // 脉冲效果的颜色  (注释*1)
        pulseLayer.fillColor = UIColor.init(r: 213, g: 54, b: 13).cgColor
        pulseLayer.opacity = 0.0
        
        // 关键代码
        let replicatorLayer = CAReplicatorLayer()
        replicatorLayer.bounds = CGRect(x: 0, y: 0, width: width, height: width)
        replicatorLayer.position = CGPoint(x: width/2, y: width/2)
        replicatorLayer.instanceCount = 3  // 三个复制图层
        replicatorLayer.instanceDelay = 1  // 频率
        replicatorLayer.addSublayer(pulseLayer)
        self.layer.addSublayer(replicatorLayer)
        self.layer.insertSublayer(replicatorLayer, at: 0)
    }
    
    func starAnimation() {
        // 透明
        let opacityAnimation = CABasicAnimation(keyPath: "opacity")
        opacityAnimation.fromValue = 1.0  // 起始值
        opacityAnimation.toValue = 0     // 结束值
        
        // 扩散动画
        let scaleAnimation = CABasicAnimation(keyPath: "transform")
        let t = CATransform3DIdentity
        scaleAnimation.fromValue = NSValue(caTransform3D: CATransform3DScale(t, 0.0, 0.0, 0.0))
        scaleAnimation.toValue = NSValue(caTransform3D: CATransform3DScale(t, 1.0, 1.0, 0.0))
        
        // 给CAShapeLayer添加组合动画
        let groupAnimation = CAAnimationGroup()
        groupAnimation.animations = [opacityAnimation,scaleAnimation]
        groupAnimation.duration = 3   //持续时间
        groupAnimation.autoreverses = false //循环效果
        groupAnimation.repeatCount = HUGE
        groupAnimation.isRemovedOnCompletion = false
        pulseLayer.add(groupAnimation, forKey: nil)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

复制代码
Swift仿写喜马拉雅FM
Swift仿写喜马拉雅FM

播放模块分析:

播放模块可以说是整个项目主线的终点,前面模块点击跳转进入具体节目界面,主页面顶部为自定义 HeaderView ,主要显示该有声读物的一些介绍,背景为毛玻璃虚化,下面为使用 LTScrollView 管理三个子模块的滚动视图,简介为对读物和作者的介绍,节目列表为该读物分章节显示,找相似为与此相似的读物,圈子为读者分享圈几个子模块都是简单的列表显示,子模块非固定是根据接口返回数据决定有哪些子模块。

点击节目列表任一 Cell 就跳转到播放详情界面,该界面采用分区 CollectionCell ,顶部 Cell 为整体的音频播放及控制,因为要实时播放音频所以没有使用 AVFoudtion ,该框架需要先缓存本地在进行播放,而是使用的三方开源的 Streaming 库来在线播放音频,剩下的为作者发言和评论等。

Swift仿写喜马拉雅FM

总结:

目前项目中主要模块的界面和功能基本完成,写法也都是比较简单的写法,项目用时很短,目前一些功能模块使用了第三方。接下来 1、准备替换为自己封装的控件 2、把项目中可以复用的部分抽离出来封装为灵活多用的公共组件 3、对当前模块进行一些 Bug 修改和当前功能完善。 在这件事情完成之后准备对整体代码进行 Review ,之后进行接下来功能模块的仿写。

最后:

感兴趣的朋友可以到 GitHubgithub.com/daomoer/XML…

下载源码看看,也请多提意见,喜欢的朋友动动小手给点个 Star :sparkles::sparkles:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python for Data Analysis

Python for Data Analysis

Wes McKinney / O'Reilly Media / 2012-11-1 / USD 39.99

Finding great data analysts is difficult. Despite the explosive growth of data in industries ranging from manufacturing and retail to high technology, finance, and healthcare, learning and accessing d......一起来看看 《Python for Data Analysis》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具