Возьмите вас за руку с домашней страницей NetEase Cloud Music (2)

iOS
Возьмите вас за руку с домашней страницей NetEase Cloud Music (2)

«Эта статья участвовала в мероприятии Haowen Convocation Order, щелкните, чтобы просмотреть:Заявки на бэк-энд и фронт-энд с двумя треками, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов! "

предисловие

Всем привет, сегодня первый день июля, помню кто-то сказал: как бы вы ни были несчастны в первой половине года, пожалуйста, не ложитесь, потому что великие карьеры завершаются во второй половине года. .

Что ж, не будем нести чушь, продолжим следить за предыдущей статьей и поговорим об этом.

Создайте инфраструктуру приложения

Сначала откройте наш Xcode, чтобы создать проект приложения на основе языка программирования Swift, и назовите его.

Наблюдая за стилем приложения NetEase Cloud Music, вы можете видеть на вкладке TabBar внизу, что его общая структура пользовательского интерфейса состоит из UITabbarController и UIViewController. Таким образом, мы можем построить общую архитектуру пользовательского интерфейса нашего приложения с помощью StoryBoard; некоторые люди могут сказать, что я не могу использовать StoryBoard, могу ли я создать его с помощью чистого кода? Конечно, да, Потому что моя привычка разработки состоит в том, чтобы перетаскивать простой пользовательский интерфейс с помощью Storyboard и писать сложный пользовательский интерфейс с кодом Это чисто личная привычка.

Визуализации, созданные с помощью Storyboard, выглядят следующим образом:

image

Создайте представление обнаружения домашней страницы

Страница, которую нам нужно построить, выглядит так:

image

На страницах, показанных выше, мы можем обнаружить, что данные, отображаемые на домашней странице NetEase Cloud Music, очень богаты, с панелью поиска, баннером с регулярной прокруткой и представлением карты с горизонтальной прокруткой, а также поддерживает Подтягивающее обновление и раскрывающееся обновление, поэтому наша домашняя страница может использовать UITableView в качестве контейнера, а затем создавать соответствующие подпредставления в ячейке, такие как баннер, UICollectionView и т. д., для реализации табличного представления домашней страницы.

Обычно, когда мы используем UITableView для загрузки данных, типы данных являются одиночными и похожими, поэтому при построении ячеек мы повторно используем одну и ту же ячейку, как в адресной книге мобильного телефона. Однако домашняя страница NetEase Cloud Music не такая: каждая ее ячейка представляет разные типы контента, что делает невозможным представление данных путем повторного использования ячеек. Итак, как мы можем построить правильное представление!

Во-первых, давайте определим проблему.

Вы можете часто видеть подобный код в других проектах для настройки UITableViewCell в соответствии с индексом в UITableView:

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
   }
   ....
}

Так что плохого в том, чтобы написать это?

Если ваше табличное представление является статическим, в нем нет переупорядочения, добавления или удаления ячеек, то с этим способом записи проблем не возникнет. Пока вы не захотите выполнить вышеуказанные операции над табличным представлением, то структура табличного представления будет вами разрушена, что потребует от вас ручного обновления всех индексов в методах cellForRowAt и didSelectRowAt.

Есть ли лучший способ?

В следующем содержании я сделаю все возможное, чтобы поделиться с вами решением этой проблемы.

MVVM

В этом проекте мы будем использовать шаблон MVVM, MVVM расшифровывается как Model-View-ViewModel, Преимущество этого шаблона в том, что представление и модель можно разделить, уменьшив связь, тем самым уменьшив размер контроллера.

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": {
                
                ...... (省略部分)
}

Теперь нам нужно создать модель, которая сопоставляет запрошенный нами JSON с созданной нами моделью. Нативные библиотеки iOS или сторонние библиотеки с открытым исходным кодом имеют множество способов анализа JSON в Swift, вы можете использовать тот, который вам нравится, например.
SwiftyJSON, HandyJSON и т. д. В этом проекте я настаиваю на использовании нативного Codable для реализации взаимного преобразования JSON/Model.

При создании модели мы также можем использовать некоторые внешние инструменты для быстрого создания модели, например, вот инструмент, который я бы вам порекомендовал:quicktype, который может генерировать соответствующую модель в соответствии с предоставленной строкой JSON, что может значительно сэкономить нам время на ручное кодирование и создание модели.

Созданная модель выглядит следующим образом:

// 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 с моделью, Поскольку библиотека 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

Модель готова, поэтому теперь нам нужно создать ViewModel, которая будет отвечать за предоставление данных нашему TableView.

Мы создадим 12 различных Разделов, а именно:

  • Banner
  • круглая кнопка
  • Рекомендуемый плейлист
  • Личная рекомендация
  • Избранные музыкальные клипы
  • Плейлист радара
  • музыкальный календарь
  • Эксклюзивный плейлист сцен
  • новая песня Юнбэя
  • Подкаст сборник
  • 24-часовой подкаст
  • Видео подборка

Поскольку данные, которые мы получаем, имеют разный формат, нам нужно использовать разные UITableViewCell для каждого типа данных, поэтому нам нужно использовать правильную структуру ViewModel.

Во-первых, мы должны различать типы данных, чтобы мы могли использовать правильную ячейку. Как отличить! Используйте if else или enum! Конечно, чтобы реализовать несколько типов в Swift и легко переключаться, лучше всего использовать перечисления, поэтому давайте начнем создавать ViewModels!

/// 类型
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     // 视频合辑
}

Каждый случай перечисления представляет другой тип данных, который нужен TableViewCell. Однако, поскольку мы хотим использовать один и тот же тип данных в обоих табличных представлениях, нам нужно абстрагироваться от этих случаев и определить один общедоступный класс, который будет определять все свойства. Здесь мы можем сделать это, используя протокол, который обеспечит вычисления свойств для нашего элемента:

protocol HomeViewModelSection {
    ...
}

Первое, что нам нужно знать, это тип элемента, поэтому нам нужно создать свойство типа для протокола и указать, является ли свойство доступным или устанавливаемым. В нашем случае тип будет HomeViewModelSection:

protocol HomeViewModelSection {
    var type: HomeViewModelSectionType { get }
}

Следующее свойство, которое нам нужно, это rowCount. Он расскажет нам, сколько строк в каждом разделе:

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

Нам также нужно добавить в протокол два свойства: rowHeight и frame. Они будут определять высоту и размеры секции:

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

Теперь мы готовы создать ViewModelItem для каждого типа данных. Каждый элемент должен соответствовать ранее определенному протоколу. Но прежде чем мы начнем, давайте сделаем еще один шаг к аккуратному и упорядоченному проекту: предоставим некоторые значения по умолчанию для нашего протокола. В swift мы можем использовать расширение расширения протокола, чтобы предоставить значение протокола по умолчанию, чтобы нам не приходилось назначать значение для rowCount каждого элемента, сохраняя некоторый избыточный код:

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

Сначала создайте 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 ViewModeItems:

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 ничего не знает о представлении, но предоставляет все данные, которые могут понадобиться представлению.

Единственное свойство ViewModel — это массив элементов, который соответствует массиву разделов, который содержит UITableView:

/// 首页 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
    }

Теперь, если вы хотите изменить порядок, добавить или удалить элементы, просто измените массив элементов этой ViewModel. Ясно, верно?

Затем мы добавляем 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
    }
}

конец

На данный момент создание проектной документации, фреймворка App UI, Model, ViewModel в основном завершено. Наконец, чтобы подвести итог, во-первых, при построении фреймворка пользовательского интерфейса приложения мы построили фреймворк пользовательского интерфейса, воспользовавшись возможностью StoryBoard быстро создавать представления; затем, в соответствии с JSON, возвращаемым интерфейсом, использовать быстрый тип внешнего инструмента преобразования. Чтобы быстро создать модель и сопоставить данные JSON с моделью, мы используем собственный код для реализации этого процесса сопоставления. Наконец, мы создаем ViewModel. Поскольку каждый из наших разделов отображает разные данные, чтобы облегчить загрузку данные из табличного представления, нам нужно выполнить загрузку данных всех разделов, абстрагированных в открытый класс для вызова, поэтому здесь мы используем протокол для работы.

Что ж, на этом статья закончилась, в следующей статье мы поговорим о том, как построить View.

Сначала прикрепите адрес проекта:GitHub.com/she NJ IE SU…В постоянном обновлении кода, если он вам нравится, не забудьте нажать звездочку ✨.

Прошлые статьи:

Пожалуйста, выпейте ☕️ Нравится + Подпишитесь~

  1. Не забудьте поставить лайк после прочтения, есть 👍 и мотивация

  2. Обратите внимание на общественный номер ---HelloWorld Цзе Шао, первый раз подтолкнуть новую позу

Оригинал статьи, написание ограничено, если есть неточности в статье, сообщите пожалуйста.