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

????

前言
一直想利用空余時間寫個開源項目契吉,兜兜轉(zhuǎn)轉(zhuǎn)許久,光說不練都是空把戲诡渴,咱擼起袖子就是干捐晶。說做咱就開始利用空閑時間騷動起來吧。本開源項目講解了一些App常見功能界面的搭建以及實現(xiàn)思路玩徊,適合新手以及正在學(xué)習(xí)Swift的同胞們租悄。

號外:最近似乎把自己活成了一個網(wǎng)癮少年般的模樣谨究,一到某個點就開始相約雞場恩袱。著實尷尬!=赫堋畔塔!

目錄
一、關(guān)于項目
二鸯屿、效果預(yù)覽
三澈吨、詳細講解
3.0 歡迎模塊分析
3.1 首頁模塊分析
3.2 我聽模塊分析
3.3 播放模塊分析
3.4 發(fā)現(xiàn)模塊分析
3.5 我的模塊分析
四、遇到的問題以及解決措施
五寄摆、總結(jié)

一谅辣、關(guān)于項目

開發(fā)環(huán)境:Xcode 9.4.1,語言:Swift4.1
代碼下載:代碼下載地址,歡迎點贊和反饋
開發(fā)備注:對此項目一些數(shù)據(jù)是使用抓包工具進行獲取,而圖片的獲取也是通過itool下載而取之婶恼。對于Charles抓包工具如果有想了解和使用的話可以查看筆者之前的文章桑阶。
然而抓包工具并不是萬能的,對于有些接口傳參方式是加密的勾邦,比如登錄,比如調(diào)用某個接口的前提是必須先登錄蚣录,然后才能掉下一個接口。咱就繞路而行吧眷篇。但是慶幸的是雖然有些接口雖不知以何種方式進行加密的萎河,但是至少能抓取到返回數(shù)據(jù),這就足夠了蕉饼。我們只要拿到返回的數(shù)據(jù)就好了虐杯。至少這個對我們只是想拿個項目練練手的話。就已經(jīng)可以算是很人性化了昧港。

喜馬拉雅FM(項目搭建及層次結(jié)構(gòu)剖析).png
項目中的方法層次圖.png

二擎椰、效果預(yù)覽

首頁.gif
我聽.gif
發(fā)現(xiàn).gif
我的.gif

三、詳細講解

3.0 歡迎模塊分析(引導(dǎo)頁和廣告頁)

當(dāng)程序被打開時,在創(chuàng)建KeyWindow的RootViewController時判斷是否是首次打開,這里的邏輯是如果用戶是首次打開應(yīng)用則顯示引導(dǎo)頁(引導(dǎo)圖片資料找了老半天沒有找著慨飘,就直接從網(wǎng)絡(luò)上拉了幾張圖片進來),當(dāng)點擊引導(dǎo)頁最后一頁的立即體驗直接進入TabBarController,不顯示廣告頁(效果如下圖)


引導(dǎo)頁.gif

如果用戶不是首次打開應(yīng)用的話,則請求網(wǎng)絡(luò)加載圖片顯示廣告頁,并且在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行确憨,至于引導(dǎo)頁和廣告頁的具體實現(xiàn)代碼,就不詳細講述了,可下載源碼進行查看


3.1 登錄模塊分析

由于登錄接口請求方式加密了译荞,故我們采取別的方式進行模擬用戶是否登錄
如下圖:


image.png

接下來打開mocky網(wǎng)站,將上圖返回的數(shù)據(jù)復(fù)制然后請繼續(xù)往下看

mocky.gif

最后就可以通過該mocky生成的接口進行網(wǎng)絡(luò)請求模擬請求了,接下來后面別的接口如遇到加密情況也將采取此種方式進行處理休弃。

緊接著對用戶登錄的一些信息進行保存吞歼,下面貼一段歸檔的代碼

//用戶模型數(shù)據(jù)
import UIKit
import HandyJSON
class UserInfoModel: NSObject,HandyJSON,NSCoding {
    required override init() { }
    var token: String? //戶授權(quán)的唯一票據(jù)
    var uid: String? //用戶的uid
    var ret: Int = 1 //返回的狀態(tài)碼  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
        
    }
}

登錄信息的數(shù)據(jù)保存以及退出登錄的本地數(shù)據(jù)清理都交由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("清除用戶數(shù)據(jù)成功"):print("清除用戶數(shù)據(jù)失敗")
    }
    
    //MARK:登錄成功
    class func loginSuccessDataHandle(){
        NotificationCenter.default.post(name: NSNotification.Name(kLoginSuccessNotification), object: nil)
    }
    //MARK:退出登錄 數(shù)據(jù)清理
    class func loginOutDataHandle() {
        LoginHelper.sharedInstance.clearUserInfo()
        NotificationCenter.default.post(name: NSNotification.Name(kLogOutNotification), object: nil)
    }
}

布局:登錄頁面布局有點粗糙,直接采取的是Xib的布局方式塔猾。

登錄成果之后保存用戶數(shù)據(jù)篙骡,再者登錄成功之后要告訴相關(guān)頁面做相應(yīng)的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 首頁模塊分析

首頁的數(shù)據(jù)較復(fù)雜一些,字母里面嵌套數(shù)組丈甸,數(shù)組里面再嵌套字典糯俗,字典又有數(shù)組字典。同時返回來的數(shù)據(jù)也相對其他接口多睦擂。首先我們先看推薦模塊的接口數(shù)據(jù)

圖片.png

頂部的banner(FMHomeHeaderView繼承于TYCyclePagerView)得湘,作為 tableView.tableHeaderView 作為處理,而(猜你喜歡顿仇、精品淘正、懶人一鍵聽)作為viewForHeaderInSection,其他部分根據(jù)判斷而返回不同樣式的Cell臼闻。

3.3 我聽模塊分析

我聽模塊頂部為自定義ListenHeaderView,下面為使用LTScrollView管理三個子模塊的滾動視圖鸿吆,訂閱接口由于涉及用戶登錄相關(guān),則拿不到實時接口述呐,故采用Mocky模擬網(wǎng)絡(luò)請求惩淳。而一鍵聽和推薦可以抓到其接口并直接可以請求到數(shù)據(jù),故直接拿的原生接口進行數(shù)據(jù)請求乓搬,且推薦頁面做了上拉刷新以及下拉加載更多思犁。訂閱中的Cell和推薦中的cell采取的公用方式,只是稍微有點不一樣而已缤谎,像這樣大致一樣的就沒有必要再寫多寫一個cell了抒倚。一鍵聽模塊其中有個跑馬燈滾動顯示的效果,點擊添加頻道坷澡,跳轉(zhuǎn)更多頻道界面托呕,(在頁面跳轉(zhuǎn)時,則統(tǒng)一在BaseNavViewController里面添加了返回按鈕频敛,以及側(cè)滑返回项郊。)該界面為雙TableView實現(xiàn)聯(lián)動效果,點擊左邊分類LeftTableView對應(yīng)右邊RightTableView滾動到指定分區(qū)斟赚,滾動右邊RightTableView對應(yīng)的左邊LeftTableView滾動到對應(yīng)分類着降。

圖片.png

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


3.5 發(fā)現(xiàn)模塊分析

發(fā)現(xiàn)頁面總成分成兩大部分,其中共涉及6個接口(綠色和藍色框框表示需要請求的接口)拗军。故采取MVVM的方式實現(xiàn)任洞,將接口請求全部放在ViewModel蓄喇,然后再根據(jù)需求進行數(shù)據(jù)回調(diào)。
因為登錄后的關(guān)注界面和推薦界面跟這個差不多交掏,所以直接服用推薦里面的cell妆偏。
未登錄的關(guān)注則采用DZNEmptyDataSet開源框架,該框架挺不錯的盅弛,筆者一直在使用钱骂。該框架目前已有1萬多顆小星星。

圖片.png

這里重點在于 圖片展示的數(shù)量計算挪鹏,以及根據(jù)文字內(nèi)容和圖片的張數(shù)計算當(dāng)前Cell的高度见秽。
實現(xiàn)思路: 這里cell主要是通過xib畫的,我們需要把collectView的寬度和高度進行拖線讨盒,然后通過圖片數(shù)量拿到對應(yīng)的寬高度解取。然后設(shè)置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對應(yīng)的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微調(diào)
            return CGSize(width: picViewWH, height: picViewWH)
        }
        
        // 3.其他張配圖 (count -1)/3 + 1  = rows
        /**   例子:  5張配圖  2行   row:(5-1)/3+1 = 2*/
        // 4.1 計算行數(shù)
        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 我的模塊分析
根據(jù)用戶是否登錄而進行一些業(yè)務(wù)邏輯的處理以及顯示肮蛹, 其中只做了掃一掃勺择,退出登錄(需清除本地數(shù)據(jù))创南,就沒做那些詳細界面了。上面的cell用xib直接畫的省核,下面的Cell根據(jù)dataArr進行分區(qū)顯示及每個分區(qū)的count稿辙。

登錄的情況下.png
未登錄的情況下.png

參考寫法

http://www.reibang.com/p/879f58fe3542

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

下廚房
http://www.reibang.com/p/a8f619a2c622


四、遇到的問題以及解決措施

問題一: 導(dǎo)入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吨娜,手機設(shè)置代理后,網(wǎng)頁無法打開
圖片.png
解決措施:
圖片.png

點擊Install Charles Root Certifficate會進入鑰匙串中淘钟,之后


4447.gif

設(shè)置完了之后就可以正常訪問網(wǎng)絡(luò)勒

問題三: import JXMarqueeView 報錯
圖片.png
解決措施:

General->Deployment Target -> 9.0 即可
最開始我的這里是8.0, 改完之后編譯一下即可


五宦赠、總結(jié)

目前項目中主要模塊的界面和功能(一級界面)基本完成,使用XIB以及SnapKit開源框架(相對于OC中的Masonry)進行布局米母。接下來
1勾扭、缺失功能的完善
2、對當(dāng)前模塊進行一些Bug修改和調(diào)整
3铁瞒、根據(jù)返回數(shù)據(jù)進行細節(jié)優(yōu)化以及調(diào)整
4妙色、少玩多學(xué),年尾已快到來慧耍,需對年初計劃進行歸納和總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末身辨,一起剝皮案震驚了整個濱河市丐谋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌煌珊,老刑警劉巖笋鄙,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怪瓶,居然都是意外死亡萧落,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門洗贰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來找岖,“玉大人,你說我怎么就攤上這事敛滋⌒聿迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵绎晃,是天一觀的道長蜜唾。 經(jīng)常有香客問我,道長庶艾,這世上最難降的妖魔是什么袁余? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮咱揍,結(jié)果婚禮上颖榜,老公的妹妹穿的比我還像新娘。我一直安慰自己煤裙,他們只是感情好掩完,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著硼砰,像睡著了一般且蓬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上题翰,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天恶阴,我揣著相機與錄音,去河邊找鬼遍愿。 笑死存淫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沼填。 我是一名探鬼主播桅咆,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坞笙!你這毒婦竟也來了岩饼?” 一聲冷哼從身側(cè)響起荚虚,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎籍茧,沒想到半個月后版述,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡寞冯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年渴析,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吮龄。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡俭茧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出漓帚,到底是詐尸還是另有隱情母债,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布尝抖,位于F島的核電站毡们,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏昧辽。R本人自食惡果不足惜衙熔,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奴迅。 院中可真熱鬧青责,春花似錦、人聲如沸取具。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暇检。三九已至,卻和暖如春婉称,著一層夾襖步出監(jiān)牢的瞬間块仆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工王暗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悔据,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓俗壹,卻偏偏與公主長得像科汗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绷雏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Summary:Run In this chapter, the author, Trevor, born in ...
    谷音sp閱讀 300評論 0 1
  • 10月24日头滔,橙思: 從現(xiàn)在開始怖亭,一切都來得及。 今日學(xué)習(xí): 又遇奇葩坤检,情商訓(xùn)練營和分身術(shù)訓(xùn)練營的生動實踐課兴猩,一把...
    NicoleYing閱讀 116評論 0 0
  • 雨聲 敲打著窗戶的, 是些零散的調(diào)子早歇, 又像一些倾芝, 煩亂的心事。 雨輕輕悄悄的箭跳, 在漆黑里墜落蛀醉, 是上帝躲著在傷心...
    萬木戀秋閱讀 396評論 0 0
  • 1 xx又失業(yè)了。 據(jù)我所知衅码,這是她第N次炒老板魷魚拯刁。 我問她,這一次又是因為什么逝段。 她說:“老板是個大傻逼垛玻,以為...
    蕭宜閱讀 475評論 2 0