Swift高仿喜马拉雅FM(Swift4.1)

😝😝

前言
一直想利用空余时间写个开源项目,兜兜转转许久,光说不练都是空把戏,咱撸起袖子就是干。说做咱就开始利用空闲时间骚动起来吧。本开源项目讲解了一些App常见功能界面的搭建以及实现思路,适合新手以及正在学习Swift的同胞们。

号外:最近似乎把自己活成了一个网瘾少年般的模样,一到某个点就开始相约鸡场。着实尴尬!!!

目录
一、关于项目
二、效果预览
三、详细讲解
3.0 欢迎模块分析
3.1 首页模块分析
3.2 我听模块分析
3.3 播放模块分析
3.4 发现模块分析
3.5 我的模块分析
四、遇到的问题以及解决措施
五、总结

一、关于项目

开发环境:Xcode 9.4.1,语言:Swift4.1
代码下载:代码下载地址,欢迎点赞和反馈
开发备注:对此项目一些数据是使用抓包工具进行获取,而图片的获取也是通过itool下载而取之。对于Charles抓包工具如果有想了解和使用的话可以查看笔者之前的文章。
然而抓包工具并不是万能的,对于有些接口传参方式是加密的,比如登录,比如调用某个接口的前提是必须先登录,然后才能掉下一个接口。咱就绕路而行吧。但是庆幸的是虽然有些接口虽不知以何种方式进行加密的,但是至少能抓取到返回数据,这就足够了。我们只要拿到返回的数据就好了。至少这个对我们只是想拿个项目练练手的话。就已经可以算是很人性化了。

喜马拉雅FM(项目搭建及层次结构剖析).png
项目中的方法层次图.png

二、效果预览

首页.gif
我听.gif
发现.gif
我的.gif

三、详细讲解

3.0 欢迎模块分析(引导页和广告页)

当程序被打开时,在创建KeyWindow的RootViewController时判断是否是首次打开,这里的逻辑是如果用户是首次打开应用则显示引导页(引导图片资料找了老半天没有找着,就直接从网络上拉了几张图片进来),当点击引导页最后一页的立即体验直接进入TabBarController,不显示广告页(效果如下图)


引导页.gif

如果用户不是首次打开应用的话,则请求网络加载图片显示广告页,并且在3秒后自动进入TabBarController(效果如下图)


广告页

逻辑代码如下:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {   
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.backgroundColor = UIColor.white
        //加载欢迎页面
        initLinkPage()
        window?.makeKeyAndVisible()
        return true
    }
    func initLinkPage() {   
        // 用来判断是否是第一次加载
        let isFristOpen = UserDefaults.standard.object(forKey: "isFristOpen")
        if isFristOpen == nil {
            let guideVC  = FMGuideViewController()
            guideVC.finishBtnClickCallBack = {[weak self]  () -> Void in
                self?.initRootViewController()
            }
            window?.rootViewController = guideVC
            UserDefaults.standard.set("isFristOpen", forKey: "isFristOpen")
        }else{
            loadAdViewController()
        }
    }
    func loadAdViewController(){
        let adVC = FMAdViewController()
        adVC.skipBtnClickCallBack = {  () -> Void in
            self.initRootViewController()
        }
        window?.rootViewController = adVC
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            self.initRootViewController()
        }
    }
    func initRootViewController() {
        window?.rootViewController = BaseTabBarViewController()
    }

这块的逻辑代码总共都不超过50行,至于引导页和广告页的具体实现代码,就不详细讲述了,可下载源码进行查看


3.1 登录模块分析

由于登录接口请求方式加密了,故我们采取别的方式进行模拟用户是否登录
如下图:


image.png

接下来打开mocky网站,将上图返回的数据复制然后请继续往下看

mocky.gif

最后就可以通过该mocky生成的接口进行网络请求模拟请求了,接下来后面别的接口如遇到加密情况也将采取此种方式进行处理。

紧接着对用户登录的一些信息进行保存,下面贴一段归档的代码

//用户模型数据
import UIKit
import HandyJSON
class UserInfoModel: NSObject,HandyJSON,NSCoding {
    required override init() { }
    var token: String? //户授权的唯一票据
    var uid: String? //用户的uid
    var ret: Int = 1 //返回的状态码  0: 表示成功
    var isLogin: Bool{
        if LoginHelper.sharedInstance.userInfo?.token != nil {
            return true
        }else {
            return false
        }
    }
    public func encode(with aCoder: NSCoder) {
        aCoder.encode(token, forKey: "token")
        aCoder.encode(uid, forKey: "uid")
    }
    required init?(coder aDecoder: NSCoder) {
        token = aDecoder.decodeObject(forKey: "token") as? String
        uid = aDecoder.decodeObject(forKey: "uid") as? String
        
    }
}

登录信息的数据保存以及退出登录的本地数据清理都交由LoginHelper来处理

import UIKit
import Foundation
var instance:LoginHelper? = nil

class LoginHelper: NSObject {
    
    var userInfo:UserInfoModel? {
        didSet{
            guard userInfo != nil else {
                return
            }
        }
    }
    static let sharedInstance: LoginHelper = {
          instance = LoginHelper()
        instance?.userInfo = UserInfoModel()
         let saveModel = NSKeyedUnarchiver.unarchiveObject(withFile: UserDataFilePath)
        
        print("path:\(UserDataFilePath)")
        if (saveModel != nil) {   
            instance?.userInfo = (saveModel as! UserInfoModel)
        }
        return instance!
        
    }()
    
    //MARK:保存用户信息
    func saveUserInfo(userInfo: UserInfoModel) {        
        NSKeyedArchiver.archiveRootObject(userInfo, toFile: UserDataFilePath)
    }
    //MARK:清除用户信息
    func clearUserInfo() {
        
        instance = nil
        userInfo?.token = nil
        let clearUserInfo:Bool = ((try?  FileManager.default.removeItem(atPath: UserDataFilePath)) != nil)
        
        clearUserInfo ? print("清除用户数据成功"):print("清除用户数据失败")
    }
    
    //MARK:登录成功
    class func loginSuccessDataHandle(){
        NotificationCenter.default.post(name: NSNotification.Name(kLoginSuccessNotification), object: nil)
    }
    //MARK:退出登录 数据清理
    class func loginOutDataHandle() {
        LoginHelper.sharedInstance.clearUserInfo()
        NotificationCenter.default.post(name: NSNotification.Name(kLogOutNotification), object: nil)
    }
}

布局:登录页面布局有点粗糙,直接采取的是Xib的布局方式。

登录成果之后保存用户数据,再者登录成功之后要告诉相关页面做相应的UI更新

        NetworkTool.shareNetworkTool().request(methodType: .GET, baseUrl: MAIN_URL_MOCKY, urlString: kLoginUrl, parameters: [:]) { (result, error) in

            guard  let resultDic  = result as? [String : AnyObject] else{
                return
            }
            let infoModel:UserInfoModel = UserInfoModel.deserialize(from: resultDic)!
            if infoModel.ret == 0 {
                LoginHelper.sharedInstance.userInfo = infoModel
                LoginHelper.sharedInstance.saveUserInfo(userInfo: infoModel)
                LoginHelper.loginSuccessDataHandle()
                self.dismiss(animated: true, completion: nil)
            }
        }

3.2 首页模块分析

首页的数据较复杂一些,字母里面嵌套数组,数组里面再嵌套字典,字典又有数组字典。同时返回来的数据也相对其他接口多。首先我们先看推荐模块的接口数据

图片.png

顶部的banner(FMHomeHeaderView继承于TYCyclePagerView),作为 tableView.tableHeaderView 作为处理,而(猜你喜欢、精品、懒人一键听)作为viewForHeaderInSection,其他部分根据判断而返回不同样式的Cell。

3.3 我听模块分析

我听模块顶部为自定义ListenHeaderView,下面为使用LTScrollView管理三个子模块的滚动视图,订阅接口由于涉及用户登录相关,则拿不到实时接口,故采用Mocky模拟网络请求。而一键听和推荐可以抓到其接口并直接可以请求到数据,故直接拿的原生接口进行数据请求,且推荐页面做了上拉刷新以及下拉加载更多。订阅中的Cell和推荐中的cell采取的公用方式,只是稍微有点不一样而已,像这样大致一样的就没有必要再写多写一个cell了。一键听模块其中有个跑马灯滚动显示的效果,点击添加频道,跳转更多频道界面,(在页面跳转时,则统一在BaseNavViewController里面添加了返回按钮,以及侧滑返回。)该界面为双TableView实现联动效果,点击左边分类LeftTableView对应右边RightTableView滚动到指定分区,滚动右边RightTableView对应的左边LeftTableView滚动到对应分类。

图片.png

3.4 播放模块分析(待完善)


3.5 发现模块分析

发现页面总成分成两大部分,其中共涉及6个接口(绿色和蓝色框框表示需要请求的接口)。故采取MVVM的方式实现,将接口请求全部放在ViewModel,然后再根据需求进行数据回调。
因为登录后的关注界面和推荐界面跟这个差不多,所以直接服用推荐里面的cell。
未登录的关注则采用DZNEmptyDataSet开源框架,该框架挺不错的,笔者一直在使用。该框架目前已有1万多颗小星星。

图片.png

这里重点在于 图片展示的数量计算,以及根据文字内容和图片的张数计算当前Cell的高度。
实现思路: 这里cell主要是通过xib画的,我们需要把collectView的宽度和高度进行拖线,然后通过图片数量拿到对应的宽高度。然后设置collectionView宽高度的constant。紧接着拿到bottomView的最大Y值, 并将高度保存到viewModel模型中


图片.png

计算图片的思路

    private func calculatePicViewSize(count:Int) -> CGSize {
        
      /**
         图片显示分几种情况:
         1.没有配图
         2.4张配图
         3.其他张配图 (count -1)/3 + 1  = rows
         */
        //1.没有配图
        if count == 0 {
            collectionViewBottomConst.constant = 0          
            return CGSize(width: 0, height: 0)
        }
        collectionViewBottomConst.constant = 10

        // 取出picView对应的layout
        let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
        //图片的WH
        let imageViewWH = (screenW - 2 * magin - 2 * iteMagin) / 3        
        layout.itemSize = CGSize(width: imageViewWH, height: imageViewWH)
        //2. 4张配图
        if  count == 4 {
            let picViewWH = imageViewWH * 2 +  iteMagin + 1  //+1微调
            return CGSize(width: picViewWH, height: picViewWH)
        }
        
        // 3.其他张配图 (count -1)/3 + 1  = rows
        /**   例子:  5张配图  2行   row:(5-1)/3+1 = 2*/
        // 4.1 计算行数
        let rows = CGFloat( (count - 1 )/3 + 1)
        //  4.2 计算高度
        let picViewH = rows * imageViewWH + (rows - 1 ) * iteMagin
        //  4.3 计算宽度
        let picViewW = screenW - 2 * magin   
        return CGSize(width: picViewW, height: picViewH)  
    }

3.6 我的模块分析
根据用户是否登录而进行一些业务逻辑的处理以及显示, 其中只做了扫一扫,退出登录(需清除本地数据),就没做那些详细界面了。上面的cell用xib直接画的,下面的Cell根据dataArr进行分区显示及每个分区的count。

登录的情况下.png
未登录的情况下.png

参考写法

//www.greatytc.com/p/879f58fe3542

喜马拉雅FM
https://juejin.im/post/5b97743df265da0af21351aa#heading-11

下厨房
//www.greatytc.com/p/a8f619a2c622


四、遇到的问题以及解决措施

问题一: 导入HandyJSON报错
111.png
原因:

"_swift_getFieldAt", referenced from:
这个问题在 HandyJSON 4.2.0 版本 + XCcode Version 9.4.1 (9F2000) + swift 4.1 里面存在,但降级到 HandyJSON 4.1.3 就没有问题了。

额外提醒:

HandyJSON 开源框架中readme 里有说明
4.2.0 版本只支持 swift 4.2, swift 4.1 就用 4.1.3 版本

解决措施:

此时我们只要在Podfile注明使用哪个基本即可 。例如:pod 'HandyJSON', '~> 4.1.3 '


问题二: MAC使用Charles,手机设置代理后,网页无法打开
图片.png
解决措施:
图片.png

点击Install Charles Root Certifficate会进入钥匙串中,之后


4447.gif

设置完了之后就可以正常访问网络勒

问题三: import JXMarqueeView 报错
图片.png
解决措施:

General->Deployment Target -> 9.0 即可
最开始我的这里是8.0, 改完之后编译一下即可


五、总结

目前项目中主要模块的界面和功能(一级界面)基本完成,使用XIB以及SnapKit开源框架(相对于OC中的Masonry)进行布局。接下来
1、缺失功能的完善
2、对当前模块进行一些Bug修改和调整
3、根据返回数据进行细节优化以及调整
4、少玩多学,年尾已快到来,需对年初计划进行归纳和总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • Summary:Run In this chapter, the author, Trevor, born in ...
    谷音sp阅读 303评论 0 1
  • 10月24日,橙思: 从现在开始,一切都来得及。 今日学习: 又遇奇葩,情商训练营和分身术训练营的生动实践课,一把...
    NicoleYing阅读 117评论 0 0
  • 雨声 敲打着窗户的, 是些零散的调子, 又像一些, 烦乱的心事。 雨轻轻悄悄的, 在漆黑里坠落, 是上帝躲着在伤心...
    万木恋秋阅读 398评论 0 0
  • 1 xx又失业了。 据我所知,这是她第N次炒老板鱿鱼。 我问她,这一次又是因为什么。 她说:“老板是个大傻逼,以为...
    萧宜阅读 480评论 2 0