关于ios:带你手把手撸一个网易云音乐首页上篇

46次阅读

共计 19821 个字符,预计需要花费 50 分钟才能阅读完成。

前言

Hello,大家好,近期我始终在学习用 Swift 编码,因为之前很多我的项目我都是用 OC 实现的,所以导致我当初对 Swift 还是处于一个学习的阶段中。为了进步本人的学习效率,每次我都会为本人定下一个短期的指标,就那这次来说吧,为了放慢本人上手 Swift, 我为本人定下了的指标就是实现一个 Swift 版本的网易云音乐 App。不晓得大家在学习一门新语言的时候,是如何进步学习效率的?无妨在评论区与大家交换一下。

调研剖析

先剖析一下 iOS 端网易云音乐 App 的首页,如图所示:

看完后,首先摆在我眼前的第一个艰难就是我该如何去获取这些数据!我的第一个想法当然就是去 GitHub 上找有没有开源的 API,不找不晓得,一找果然很称心,原来早就有大佬提供了网易云音乐的 API:

其中就有“首页发现”和“首页 - 发现 - 圆形图标入口列表”的 API, 无需咱们进行多个接口的调用以及数据源的拼接,就可一获取首页的全副数据啦!在剖析返回的 JSON 数据格式的时候,还给大佬提了个 issue,大佬也很快的回复了,再次膜拜一下大佬。

{
    "code": 200,
    "data": {
        "cursor": null,
        "blocks": [
            {
                "blockCode": "HOMEPAGE_BANNER",
                "showType": "BANNER",
                "extInfo": {
                    "banners": [
                        {
                            "adLocation": null,
                            "monitorImpress": null,
                            "bannerId": "1622653251261138",
                            "extMonitor": null,
                            "pid": null,
                            "pic": "http://p1.music.126.net/gWmqDS3Os7FWFkJ3s8Wotw==/109951166052270907.jpg",
                            "program": null,
                            "video": null,
                            "adurlV2": null,
                            "adDispatchJson": null,
                            "dynamicVideoData": null,
                            "monitorType": null,
                            "adid": null,
                            "titleColor": "red",
                            "requestId": "","exclusive": false,"scm":"1.music-homepage.homepage_banner_force.banner.2941964.-1777659412.null","event": null,"alg": null,"song": {......}

数据源的问题解决了,接下来就是该解决如何将数据可视化了,从网易云音乐首页展现的成果剖析来看,整体的视图反对高低滚动,其中单个 Cell 的视图反对横向滚动,所以这里采纳 UITableView 嵌套 UICollectionView 的形式应该来说再适合不过了。

剩下的就是须要用到的一些第三方库了,在这里咱们用到的第三方库如下:

  • Alamofire
  • Kingfisher
  • SnapKit

须要实现的性能

作为一个开发者,有一个学习的气氛跟一个交换圈子特地重要,这是一个我的 iOS 开发交换群:710 558 675,不论你是小白还是大牛都欢送入驻,让咱们一起提高,独特倒退!(群内会收费提供一些群主珍藏的收费学习书籍材料以及整顿好的几百道面试题和答案文档!)

它的首页内容大抵能够分为以下几局部:

  1. 顶部搜寻视图
  2. Banner
  3. 圆形菜单按钮
  4. 举荐歌单
  5. 共性举荐
  6. 精选音乐视频
  7. 雷达歌单
  8. 热门播客
  9. 专属场景歌单
  10. 新歌,新碟,数字专辑
  11. 音乐日历
  12. 24 小时播客
  13. 视频合辑

反对 light Mode 和 Dark Mode 主题

这里先放上我最终实现好了的效果图:

具体的实现细节我会通过两篇文章论述,性能会依照我上述列出来的性能程序来一一实现的,废话不多说,咱们持续来接着来往下讲。

构建 App 框架

首先关上咱们的 Xcode 创立一个基于 Swift 编程语言的 App 工程,并将它命名。

通过观察网易云音乐 App 的款式,从底部的 TabBar 即可看出它整体的 UI 框架是由 UITabbarController 和 UIViewController 组成的,所以咱们能够通过 StoryBoard 将咱们的 App 的整体 UI 架构搭建起来;有的人可能会说我不会用 StoryBoard,我用纯代码能够搭建吗?答案当然是能够的,因为我的开发习惯就是简略的 UI 用 Storyboard 拖拖拽拽,简单的 UI 用代码编写,这纯属于集体习惯,怎么适宜本人怎么来就行。

应用 Storyboard 搭建的效果图如下:

构建首页发现视图

咱们须要构建的页面是这样的:

通过下面展现的页面,咱们能够发现网易云音乐的首页内容展现的数据十分的丰盛,有搜寻栏,有定时滚动的 Banner,有横向滚动的卡片视图,本身还反对 上拉刷新和下拉刷新,所以咱们的首页能够采纳 UITableView 来作为容器,而后在 Cell 上构建相应的子视图,例如 Banner, UICollectionView 等,来实现首页这一表视图。

通常咱们在用 UITableView 加载数据的时候,数据的类型都是繁多相似的,所以咱们在构建 Cell 的时候,都是复用的同一个 Cell,相似手机通讯录一样。然而网易云音乐首页可不是那么回事了,它的每个 Cell 出现的内容类型都是不同的,这就导致咱们无奈通过复用 Cell 的形式来出现数据了,那怎么样能力构建出正确的视图呢!

首先,咱们先来确定问题。

你或者能够常常在别的我的项目中看到这样的代码,在 UITableView 中依据 index 来配置 UITableViewCell:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

   if indexPath.row == 0 {//configure cell type 1} else if indexPath.row == 1 {//configure cell type 2}
   ....
}

同样的在代理办法 didSelectRowAt 中应用同样的逻辑:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

if indexPath.row == 0 {//configure action when tap cell 1} else if indexPath.row == 1 {//configure action when tap cell 1}
   ....
}

那这么写有什么问题吗?

如果你的这个表视图是动态的,不存在从新排序或者在表视图里增加或删除 Cell,那么这样写一点问题也没有。直到你想对表视图进行下面所说的这些操作的时候,那么表视图的构造都将被你毁坏,这就须要你手动去更新 cellForRowAt 和 didSelectRowAt 办法中所有的 index 了。

那有什么更好的方法吗?

在接下来的内容中,我会尽我所能与大家分享这个问题的解决思路。

MVVM

在这个我的项目中,咱们将应用 MVVM 模式,MVVM 代表 Model-View-ViewModel, 这种模式的益处在于能够让视图与模型独立进去,升高耦合,从而来加重 Controller 的体积。

Model

在上一篇文章中,咱们曾经确定了获取数据源的接口,接下来就是如何去申请数据了?

在这里我用到的网路申请库是一个第三方的开源库:Alamofire,简略的将它的申请接口封装一下,代码如下:

import UIKit
import Alamofire

enum MethodType {
    case get
    case post
}

enum NetworkError: Error {
    case invalidResponse
    case nilResponse
}

class NetworkManager<T: Codable> {
    // 网络申请
    static func requestData(_ type: MethodType,
                           URLString: String,
                           parameters: [String : Any]?,
                           completion: @escaping (Result<T, NetworkError>) -> Void) {

        let method = type == .get ? HTTPMethod.get : HTTPMethod.post

        AF.request(URLString, method: method, parameters: parameters, encoding: URLEncoding.httpBody)
            .validate()
            .responseDecodable(of: T.self) { response in
                if let value = response.value {completion(.success(value))
                    return
                }

                if let error = response.error {completion(.failure(.invalidResponse))
                    return
                }

                completion(.failure(.nilResponse))
        }
    }
}

申请返回的 JSON 数据格式如下:

{
    "code": 200,
    "data": {
        "cursor": null,
        "blocks": [
            {
                "blockCode": "HOMEPAGE_BANNER",
                "showType": "BANNER",
                "extInfo": {
                    "banners": [
                        {
                            "adLocation": null,
                            "monitorImpress": null,
                            "bannerId": "1622653251261138",
                            "extMonitor": null,
                            "pid": null,
                            "pic": "http://p1.music.126.net/gWmqDS3Os7FWFkJ3s8Wotw==/109951166052270907.jpg",
                            "program": null,
                            "video": null,
                            "adurlV2": null,
                            "adDispatchJson": null,
                            "dynamicVideoData": null,
                            "monitorType": null,
                            "adid": null,
                            "titleColor": "red",
                            "requestId": "","exclusive": false,"scm":"1.music-homepage.homepage_banner_force.banner.2941964.-1777659412.null","event": null,"alg": null,"song": {...... ( 省略局部)
}

当初,咱们须要创立一个 Model, 将咱们申请到的 JSON 映射到咱们创立的 Model 上。iOS 原生或第三方开源库有许多能够在 Swift 中解析 JSON 的形式,你能够应用你喜爱的那个, 例如
SwiftyJSON,HandyJSON 等,在这个工程中,我保持应用原生的 Codable 来实现 JSON/Model 的互相转换。

在创立 Model 的时候,咱们还能够利用一些内部的工具,来疾速的创立 Model,比方在这里我要举荐给大家的一个工具:quicktype,它能够依据提供的 JSON 字符串生成相应的 Model, 能够很大水平上节约咱们手动编码创立 Model 的工夫。

创立的 Model 如下:

// MARK: - Welcome
struct HomePage: Codable {
    let code: Int
    let data: DataClass
    let message: String
}

// MARK: - DataClass
struct DataClass: Codable {
    let cursor: JSONNull?
    let blocks: [Block]
    let hasMore: Bool
    let blockUUIDs: JSONNull?
    let pageConfig: PageConfig
    let guideToast: GuideToast
}

// MARK: - Block
struct Block: Codable {
    let blockCode, showType: String
    let extInfo: EXTInfoUnion?
    let canClose: Bool
    let action: String?
    let actionType: ActionType?
    let uiElement: BlockUIElement?
    let creatives: [Creative]?
}

enum ActionType: String, Codable {
    case clientCustomized = "client_customized"
    case orpheus = "orpheus"
}

// MARK: - Creative
struct Creative: Codable {
    let creativeType: String
    let creativeID, action: String?
    let actionType: ActionType?
    let uiElement: CreativeUIElement?
    let resources: [ResourceElement]?
    let alg: String?
    let position: Int
    let code: String?
    let logInfo: String? = ""
    let creativeEXTInfoVO: CreativeEXTInfoVO?
    let source: String?

    enum CodingKeys: String, CodingKey {
        case creativeType
        case creativeID = "creativeId"
        case action, actionType, uiElement, resources, alg, position, code
        case creativeEXTInfoVO = "creativeExtInfoVO"
        case source
    }
}

// MARK: - CreativeEXTInfoVO
struct CreativeEXTInfoVO: Codable {let playCount: Int}

// MARK: - ResourceElement
struct ResourceElement: Codable {
    let uiElement: ResourceUIElement
    let resourceType: String
    let resourceID: String
    let resourceURL: String?
    let resourceEXTInfo: ResourceEXTInfo?
    let action: String
    let actionType: ActionType
    let valid: Bool
    let alg: String?
    let logInfo: String? = ""

    enum CodingKeys: String, CodingKey {
        case uiElement, resourceType
        case resourceID = "resourceId"
        case resourceURL = "resourceUrl"
        case resourceEXTInfo = "resourceExtInfo"
        case action, actionType, valid, alg
    }
}

........(因为代码篇幅过长,省略局部)复制代码 

接下来,咱们开始将 JSON 映射到 Model 中,因为 Alamofire 库曾经提供了 Codable, 所以咱们只须要解决它的返回值即可:

    NetworkManager<Menus>.requestData(.get, URLString: NeteaseURL.Menu.urlString, parameters: nil) { result in
        switch result {case .success(let response):
            let data: [Datum] = response.data
            let model: MenusModel = MenusModel(data: data)
        case .failure(let error):
           print(error.localizedDescription)
        }
    }

ViewModel

Model 已筹备结束,所以接下来咱们须要创立 ViewModel,它将负责向咱们的 TableView 表视图提供数据。

咱们将创立 12 个不同的 Sections,别离是:

  • Banner
  • 圆形按钮
  • 举荐歌单
  • 共性举荐
  • 精选音乐视频
  • 雷达歌单
  • 音乐日历
  • 专属场景歌单
  • 云贝新歌
  • 播客合辑
  • 24 小时播客
  • 视频合辑

因为咱们获取到的数据都不是同一格局的,所以咱们须要对每种类型的数据应用不同的 UITableViewCell,因而咱们须要应用正确的 ViewModel 构造。

首先,咱们必须辨别数据类型,以便于咱们能够应用正确的 Cell。那该如何去辨别呢!是用 if else 还是用 enum 呢!当然在 Swift 中要实现多种类型并且能够轻松切换,最好的形式还是应用枚举,那么就让咱们开始构建 ViewModel 吧!

/// 类型
enum HomeViewModelSectionType {
    case BANNER             // Banner
    case MENUS              // 圆形按钮
    case PLAYLIST_RCMD      // 举荐歌单
    case STYLE_RCMD         // 共性举荐
    case MUSIC_MLOG         // 精选音乐视频
    case MGC_PLAYLIST       // 雷达歌单
    case MUSIC_CALENDAR     // 音乐日历
    case OFFICIAL_PLAYLIST  // 专属场景歌单
    case ALBUM_NEW_SONG     // 云贝新歌
    case VOICELIST_RCMD     // 播客合辑
    case PODCAST24          // 24 小时播客
    case VIDEO_PLAYLIST     // 视频合辑
}

每个 enum case 示意 TableViewCell 须要的不同的数据类型。然而,因为咱们心愿在表视图中都应用雷同类型的数据,所以咱们须要将这些 case 都形象进去,定义一个独自的公共类,它将决定所有属性。在这里,咱们能够通过应用协定来实现这一点,该协定将为咱们的 item 提供属性计算:

protocol HomeViewModelSection {...}

首先,咱们须要晓得的是 item 的类型, 因而咱们须要为协定创立一个类型属性,并指定该属性是 gettable 还是 settable。在咱们的例子中,类型将是 HomeViewModelSection:

protocol HomeViewModelSection {var type: HomeViewModelSectionType { get}
}

咱们须要的下一个属性是 rowCount。它将通知咱们每个 section 有多少行:

protocol HomeViewModelSection {var type: HomeViewModelSectionType { get}
    var rowCount: Int {get}
}

咱们还须要在协定中增加俩个属性,别离是 rowHeight 和 frame。它们将定义 Section 的高度和尺寸:

protocol HomeViewModelSection {var type: HomeViewModelSectionType { get}
    var rowCount: Int {get}
    var rowHeight: CGFloat {get}
    var frame: CGRect {get set}
}

当初,咱们曾经筹备好为每种数据类型创立 ViewModelItem。每个 item 都须要恪守后面定义好的协定。但在咱们开始之前,让咱们再向简洁有序的我的项目迈出一步:为咱们的协定提供一些默认值。在 swift 中,咱们能够应用协定扩大 extension 为协定提供默认值, 这样咱们就不用为每个 item 的 rowCount 赋值了,省去一些冗余的代码:

extension HomeViewModelSection {
    var rowCount: Int {return 1}
}

先为 Banner Cell 创立一个 ViewModeItem:

import Foundation
import UIKit

class BannerModel: HomeViewModelSection {
    var frame: CGRect

    var type: HomeViewModelSectionType {return .BANNER}

    var rowCount: Int{return 1}

    var rowHeight:CGFloat

    var banners: [Banner]!

    init(banners: [Banner]) {
        self.banners = banners
        self.frame = BannerModel.caculateFrame()
        self.rowHeight = self.frame.size.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionD_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

而后咱们能够创立残余的 11 个 ViewModeItem:

class MenusModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .MENUS}

    var rowCount: Int{return 1}

    var data: [Datum]!

    init(data: [Datum]) {
        self.data = data
        self.frame = MenusModel.caculateFrame()
        self.rowHeight = self.frame.size.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionC_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class MgcPlaylistModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .MGC_PLAYLIST}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = MgcPlaylistModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class StyleRcmdModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .STYLE_RCMD}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = StyleRcmdModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionE_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class PlaylistRcmdModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .PLAYLIST_RCMD}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = PlaylistRcmdModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class MusicMLOGModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .MUSIC_MLOG}

    var rowCount: Int{return 1}

    var uiElement: BlockUIElement?
    var mLog: [EXTInfoElement]!

    init(mLog: [EXTInfoElement], ui elements: BlockUIElement) {
        self.mLog = mLog
        self.uiElement = elements
        self.frame = MusicMLOGModel.caculateFrame()
        self.rowHeight = self.frame.size.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class OfficialPlaylistModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .OFFICIAL_PLAYLIST}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = OfficialPlaylistModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class MusicCalendarModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .MUSIC_CALENDAR}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = MusicCalendarModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionB_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class AlbumNewSongModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .ALBUM_NEW_SONG}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = AlbumNewSongModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class Podcast24Model: HomeViewModelSection
{
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .PODCAST24}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = Podcast24Model.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class VoiceListRcmdModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .VOICELIST_RCMD}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = VoiceListRcmdModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

class VideoPlaylistModel: HomeViewModelSection {
    var rowHeight: CGFloat

    var frame: CGRect

    var type: HomeViewModelSectionType {return .VIDEO_PLAYLIST}

    var rowCount: Int{return 1}

    var creatives: [Creative]!
    var uiElement: BlockUIElement?

    init(creatives: [Creative], ui elements: BlockUIElement) {
        self.creatives = creatives
        self.uiElement = elements
        self.frame = VideoPlaylistModel.caculateFrame()
        self.rowHeight = self.frame.height
    }

    /// 依据模型计算 View frame
    class func caculateFrame() -> CGRect {let height: CGFloat = sectionA_height * CGFloat(scaleW)
        let width: CGFloat = CGFloat(kScreenWidth)
        return CGRect(x: 0, y: 0, width: width, height: height)
    }
}

这就是数据项所需的全部内容。

最初一步是创立 ViewModel 类。这个类能够被任何 ViewController 应用,这也是 MVVM 构造背地的要害思维之一:你的 ViewModel 对 View 无所不知,但它提供了 View 可能须要的所有数据。

ViewModel 领有的惟一属性是 item 数组,它对应着 UITableView 蕴含的 section 数组:

/// 首页 ViewModel
class HomeViewModel: NSObject {var sections = [HomeViewModelSection]()}

首先,咱们先初始化 ViewModel,将获取到的数据存储到数组中:

/// 首页 ViewModel
class HomeViewModel: NSObject {var sections = [HomeViewModelSection]()
    weak var delegate: HomeViewModelDelegate?

    override init() {super.init()
        fetchData()}

    // 获取首页数据,异步申请并将数据配置好
    func fetchData() {
        // 1. 创立工作组
        let queueGroup = DispatchGroup()
        // 2. 获取首页数据
        queueGroup.enter()
        // 申请数据 首页发现 + 圆形图片

        NetworkManager<HomePage>.requestData(.get, URLString: NeteaseURL.Home.urlString, parameters: nil) { result in
            switch result {case .success(let response):
                // 拆分数据模型到各个板块
                self.sections = self.splitData(data: response.data.blocks)
                queueGroup.leave()
            case .failure(let error):
                print(error.localizedDescription)
                self.delegate?.onFetchFailed(with: error.localizedDescription)
                queueGroup.leave()}
        }

        // 3\. 异步获取首页圆形按钮
        queueGroup.enter()
        NetworkManager<Menus>.requestData(.get, URLString: NeteaseURL.Menu.urlString, parameters: nil) { result in
            switch result {case .success(let response):
                // 拆分数据模型到各个板块
                let data: [Datum] = response.data
                let model: MenusModel = MenusModel(data: data)
                if self.sections.count > 0 {self.sections.insert(model, at: 1)
                }
                queueGroup.leave()
            case .failure(let error):
                print(error.localizedDescription)
                self.delegate?.onFetchFailed(with: error.localizedDescription)
                queueGroup.leave()}
        }

        // 4\. 执行后果
        queueGroup.notify(qos: .default, flags: [], queue: .main) {
            // 数据回调给 view, 完结 loading 并加载数据
            self.delegate?.onFetchComplete()}

    }
}

而后再基于 ViewModelItem 的属性类型,配置须要显示的 ViewModel。

/// 拆分已解析好的数据到各个数据模型
    /// - Parameter data: 首页发现数据模型
    func splitData(data: [Block]) -> [HomeViewModelSection]{var array: [HomeViewModelSection] = [HomeViewModelSection]()

        for item in data {
            if item.blockCode == "HOMEPAGE_BANNER" || item.blockCode == "HOMEPAGE_MUSIC_MLOG"{
                switch item.extInfo {case .extInfoElementArray(let result):
                    // 精选音乐视频
                    let model: MusicMLOGModel = MusicMLOGModel(mLog: result, ui: item.uiElement!)
                    array.append(model)
                    break
                case .purpleEXTInfo(let result):
                    // BANNER
                    let banner: [Banner] = result.banners
                    let model: BannerModel = BannerModel(banners: banner)
                    array.append(model)
                    break
                case .none:
                    break
                }
            } else if item.blockCode == "HOMEPAGE_BLOCK_PLAYLIST_RCMD" {
                // 举荐歌单
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model: PlaylistRcmdModel = PlaylistRcmdModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_BLOCK_STYLE_RCMD" {
                // 共性举荐
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model:StyleRcmdModel = StyleRcmdModel(creatives: creatives, ui: ui)
                array.append(model)
            }  else if item.blockCode == "HOMEPAGE_BLOCK_MGC_PLAYLIST" {
                // 网易云音乐的雷达歌单
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model:MgcPlaylistModel = MgcPlaylistModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_MUSIC_CALENDAR" {
                // 音乐日历
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model:MusicCalendarModel = MusicCalendarModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_BLOCK_OFFICIAL_PLAYLIST" {
                // 专属场景歌单
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model:OfficialPlaylistModel = OfficialPlaylistModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_BLOCK_NEW_ALBUM_NEW_SONG" {
                // 新歌
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model: AlbumNewSongModel = AlbumNewSongModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_VOICELIST_RCMD" {
                // 播客合辑
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model: VoiceListRcmdModel = VoiceListRcmdModel(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_PODCAST24" {
                // 24 小时播客
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model: Podcast24Model = Podcast24Model(creatives: creatives, ui: ui)
                array.append(model)
            } else if item.blockCode == "HOMEPAGE_BLOCK_VIDEO_PLAYLIST" {
                // 视频合辑
                let ui: BlockUIElement = item.uiElement!
                let creatives: [Creative] = item.creatives!
                let model: VideoPlaylistModel = VideoPlaylistModel(creatives: creatives, ui: ui)
                array.append(model)
            }
        }

        return array
    }

当初,如果要从新排序、增加或删除 item,只需批改此 ViewModel 的 item 数组即可。很分明,是吧?

接下来,咱们将 UITableViewDataSource 增加到 ModelView:

extension DiscoveryViewController {
    // Mark UITableViewDataSource
    override func numberOfSections(in tableView: UITableView) -> Int {
        if homeViewModel.sections.isEmpty {return 0}
        return homeViewModel.sections.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{return homeViewModel.sections[section].rowCount
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {// configure the cells here}
}

结尾

作为一个开发者,有一个学习的气氛跟一个交换圈子特地重要,这是一个我的 iOS 开发交换群:710 558 675,不论你是小白还是大牛都欢送入驻,让咱们一起提高,独特倒退!(群内会收费提供一些群主珍藏的收费学习书籍材料以及整顿好的几百道面试题和答案文档!)

到此,创立我的项目工程, App UI 框架,Model, ViewModel 曾经根本实现。最初再总结一下,首先在构建 App UI 框架的时候,咱们利用 StoryBoard 能疾速构建视图的特点搭建了 UI 框架;而后,依据接口返回的 JSON,利用内部转换工具 quicktype 疾速生成 Model, 将 JSON 数据映射到 Model 上,咱们应用了原生的 Codable 来实现这一映射过程,最初,创立 ViewModel,因为咱们的每个 Section 展现的数据都不同,为了不便表视图加载数据,就须要对所有的 Section 加载的数据进行形象成一个公共类以便调用,所以这里咱们应用了协定来解决。

好了,这篇文章到此就完结了,下篇文章咱们来讲一下如何构建 View。

正文完
 0