Swift仿寫喜馬拉雅FM

前言:

  • 最近抽空面了幾家公司,大部分都是從基礎開始慢慢深刻項目和原理。面試內容仍是以OC爲主,可是多數也都會問一下Swift技術狀況,也有例外全程問Swift的公司(作區塊鏈項目),感受如今雖然大多數公司任然以OC作爲主開發語言,可是Swift發展很強勢,估計明年Swift5之後使用會更加普遍。
  • 另外,若是準備跳槽的話,能夠提早投簡歷抽空面試幾家公司,一方面能夠經過投遞反饋檢驗簡歷,另外能夠總結面試的大體問題方向有利於作針對性複習,畢竟會用也要會說才行,會說也要能說到重點才行,還有就是心儀的公司必定要留到最後面試。但願都能進一個心儀不坑的公司,固然也應努力提高本身的技術,不坑公司不坑團隊, 好像跑題了!!!

目錄:

關於項目:

該項目採用MVC+MVVM設計模式,Moya+SwiftyJSON+HandyJSON網絡框架和數據解析。數據來源抓包及部分本地json文件。 使用Xcode9.4基於Swift4.1進行開發。 項目中使用到的一些開源庫如下列表,在這裏感謝做者的開源。git

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'
複製代碼

效果圖:

首頁
分類
我聽
發現
個人
播放
項目按照 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看起來更整潔一點,避免滿眼的邏輯判斷。github

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,經過定時器控制器自動上下滾動。 web

推薦
首頁分區
首頁推薦的其餘模塊根據接口請求獲得的 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顯示不一樣類型Celljson

分類.png
分類

首頁Vip模塊分析:

首頁Vip模塊與推薦模塊較爲類似,頂部Banner滾動圖片和分類按鈕做爲頂部Cell,而後其餘Cell橫向顯示或者是豎向顯示以及顯示的Item數量根據接口而定,分區的標題一樣來自於接口數據,點擊分區headerVeiw的更多按鈕跳轉到該分區模塊的更多頁面。 swift

Vip

首頁直播模塊分析:

首頁直播界面的排版主要分四個部分也就是自定義四個CollectionCell,頂部分類按鈕,接着是Banner滾動圖片Cell內部使用FSPagerView實現滾動圖片效果,滾動排行榜爲Cell內部嵌套CollectionView,經過定時器控制CollectionCell實現自動滾動,接下來就是播放列表了,經過自定義HeaderView上面的按鈕切換,刷新不一樣類型的播放列表。 設計模式

live.gif
直播.png

首頁廣播模塊分析:

首頁廣播模塊主要分三個部分,頂部分類按鈕Cell,中間可展開收起分類Item,由於接口中返回的是14個電臺分類,收起狀態顯示7個電臺和展開按鈕,展開狀態顯示14個電臺和收起按鈕中間空一格Item,在ViewModel中獲取到數據以後進行插入圖片按鈕並根據當前展開或是收起狀態返回不一樣Item數據來實現這部分功能,剩下的是根據數據接口中的分區顯示列表和HeaderView內容。api

點擊廣播頂部分類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
   }
複製代碼

首頁-廣播

我聽模塊分析:

我聽模塊主頁面頂部爲自定義HeaderView,內部循環建立按鈕,下面爲使用LTScrollView管理三個子模塊的滾動視圖,訂閱和推薦爲固定列表顯示接口數據,一鍵聽模塊也是現實列表數據,其中有個跑馬燈滾動顯示重要內容的效果,點擊添加頻道,跳轉更多頻道界面,該界面爲雙TableView實現聯動效果,點擊左邊分類LeftTableView對應右邊RightTableView滾動到指定分區,滾動右邊RightTableView對應的左邊LeftTableView滾動到對應分類。 bash

listen.gif

發現模塊分析:

發現模塊主頁面頂部爲自定義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)
    }
複製代碼

發現.png

個人模塊分析:

個人界面在這裏被劃分爲了三個模塊,頂部的頭像、名稱、粉絲等一類我的信息做爲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)
    }
}

複製代碼

個人.gif
個人.png

播放模塊分析:

播放模塊能夠說是整個項目主線的終點,前面模塊點擊跳轉進入具體節目界面,主頁面頂部爲自定義HeaderView,主要顯示該有聲讀物的一些介紹,背景爲毛玻璃虛化,下面爲使用LTScrollView管理三個子模塊的滾動視圖,簡介爲對讀物和做者的介紹,節目列表爲該讀物分章節顯示,找類似爲與此類似的讀物,圈子爲讀者分享圈幾個子模塊都是簡單的列表顯示,子模塊非固定是根據接口返回數據決定有哪些子模塊。

點擊節目列表任一Cell就跳轉到播放詳情界面,該界面採用分區CollectionCell,頂部Cell爲總體的音頻播放及控制,由於要實時播放音頻因此沒有使用AVFoudtion,該框架須要先緩存本地在進行播放,而是使用的三方開源的Streaming庫來在線播放音頻,剩下的爲做者發言和評論等。

play.gif

總結:

目前項目中主要模塊的界面和功能基本完成,寫法也都是比較簡單的寫法,項目用時很短,目前一些功能模塊使用了第三方。接下來 一、準備替換爲本身封裝的控件 二、把項目中能夠複用的部分抽離出來封裝爲靈活多用的公共組件 三、對當前模塊進行一些Bug修改和當前功能完善。 在這件事情完成以後準備對總體代碼進行Review,以後進行接下來功能模塊的仿寫。

最後:

感興趣的朋友能夠到GitHubgithub.com/daomoer/XML…
下載源碼看看,也請多提意見,喜歡的朋友動動小手給點個Star✨✨

相關文章
相關標籤/搜索