RxSwift & MVVM

image.png

簡單說下在MVVM架構(gòu)下使用RxSwift的思路:
ViewController在這個架構(gòu)中,也是屬于View這個層級。

首先,假設(shè)需要搭建一個UI界面,而且這個頁面需要向API發(fā)送請求,獲取數(shù)據(jù)來展示UI界面。

  1. View 會把需要的請求參數(shù)以observable的形式讓 ViewModel 接收到(不管是綁定還是訂閱)
  2. ViewModel 從接收到的observable拿到所需要的數(shù)據(jù),調(diào)用 APIManager, 獲取到類型 Modelobservable,也就是observable<Model> 或者 observable<[Model]>
  3. ViewModel 把對應(yīng)類型 Modelobservable輸出給 View 使用

可以看到 ViewModel 在整個過程中,將對應(yīng)的參數(shù),通過網(wǎng)絡(luò)請求以及數(shù)據(jù)轉(zhuǎn)換,輸出 View 需要的 Model 類型數(shù)據(jù)。


以下是Demo
Demo 地址 ?? >>>WangYiNewsRxSwiftDemo

拿了網(wǎng)易新聞的API 來做這個Demo.gif
介紹一些用到的第三方庫
target 'WangYiNews' do
  use_frameworks!
  pod 'RxSwift'
  pod 'RxCocoa'
  pod 'SnapKit'         # 跟Masonry一樣是用來設(shè)置約束的,swift版
  pod 'SwiftyJSON'      # Json數(shù)據(jù)轉(zhuǎn)換
  pod 'Alamofire'       # 用于網(wǎng)絡(luò)請求
  pod 'Moya/RxSwift'    # 用于網(wǎng)絡(luò)請求
  pod 'Kingfisher'      # SDWebImage swift 版
  pod 'RxDataSources', '~> 3.0'  # RxSwift中用于設(shè)置UITableView/UICollectionView data sources
end
文件分類.png

》》代碼《《

Model 設(shè)計:

很簡單,demo頁面只需要這些, imgnewextra 數(shù)組是用來存儲三圖的情況

import UIKit

struct NewsModel {
    var title: String
    var imgsrc: String
    var replyCount: String
    var source: String
    var imgnewextra: [Imgnewextra]?
}

struct Imgnewextra {
    var imgsrc: String
}


ViewModel 設(shè)計:

API請求只需要一個offset的參數(shù),用于獲取offset參數(shù)之后10條新聞, 所以input只需要一個Variable, output對于這個頁面來說,只是需要一個model數(shù)組,用于展示新聞列表。

import RxSwift
import RxCocoa

class NewsViewModel: NSObject {
    // input
    let offset = Variable("")
    
    // output
    var newsData: Driver<[NewsSections]> {
        return offset.asObservable()
            .throttle(0.3, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .flatMap(NewsDataManager.shared.getNews)
            .asDriver(onErrorJustReturn: [])
    }

}

output這里的newsDatainput拿到offset,通過網(wǎng)絡(luò)請求和數(shù)據(jù)轉(zhuǎn)換變成 driver<[NewsSections]>的代碼,是不可能一步到位的,此處只是附上最終調(diào)用APIManager代碼之后的完整代碼。
所以,知道input能拿到什么之后,就可以去設(shè)計APIManager, 也就是網(wǎng)絡(luò)層。


APIManager(網(wǎng)絡(luò)層) 設(shè)計:

網(wǎng)絡(luò)層使用了 Moya, Alamofire, SwiftyJSON 這幾種常用的第三方庫,要明確一點就是API request之后這個 APIManager 到底要輸出什么?在這里也就是 Observable<[NewsSections]>

import RxCocoa
import RxSwift
import Moya
import Alamofire
import SwiftyJSON

class NewsDataManager: NSObject {
    
    static let shared = NewsDataManager()
    
    private let provider = MoyaProvider<NewsMoya>()
    
    func getNews(_ offset: String) -> Observable<[NewsSections]> {
        return Observable<[NewsSections]>.create ({ observable in
            self.provider.request(.news(offset), callbackQueue: DispatchQueue.main) { response in
                switch response {
                case let .success(results):
                    let news = self.parse(results.data)
                    observable.onNext(news)
                    observable.onCompleted()
                case let .failure(error):
                    observable.onError(error)
                }
            }
            return Disposables.create()
        })
    }
    
    func parse(_ data: Any) -> [NewsSections] {
        guard let json = JSON(data)["T1348649079062"].array else { return [] }
        var news: [NewsModel] = []
        json.forEach {
            guard !$0.isEmpty else { return }
            var imgnewextras: [Imgnewextra] = []
            if let imgnewextraJsonArray = $0["imgnewextra"].array {
                imgnewextraJsonArray.forEach {
                    let subItem = Imgnewextra(imgsrc: $0["imgsrc"].string ?? "")
                    imgnewextras.append(subItem)
                }
            }
            let new = NewsModel(title: $0["title"].string ?? "", imgsrc: $0["imgsrc"].string ?? "", replyCount: $0["replyCount"].string ?? "", source: $0["source"].string ?? "", imgnewextra: imgnewextras)
            
            news.append(new)
        }
        return [NewsSections(header: "1", items: news)]
    }

}


enum NewsMoya {
    case news(_ offset: String)
}


extension NewsMoya: TargetType {
    var baseURL: URL {
        return URL(string: "https://c.m.163.com")!
    }
    
    var path: String {
        return "/dlist/article/dynamic"
    }
    
    var method: HTTPMethod {
        return .get
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case let .news(offset):
            let parameters = ["from": "T1348649079062", "devId": "H71eTNJGhoHeNbKnjt0%2FX2k6hFppOjLRQVQYN2Jjzkk3BZuTjJ4PDLtGGUMSK%2B55", "version": "54.6", "spever": "false", "net": "wifi", "ts": "\(Date().timeStamp)", "sign": "BWGagUrUhlZUMPTqLxc2PSPJUoVaDp7JSdYzqUAy9WZ48ErR02zJ6%2FKXOnxX046I", "encryption": "1", "canal": "appstore", "offset": offset, "size": "10", "fn": "3"]
            return .requestParameters(parameters: parameters, encoding: URLEncoding.default)
        }
    }
    
    var headers: [String : String]? {
        return ["Content-Type": "text/plain"]
    }
    
    
}


回到View層:
  1. View能提供給ViewModel一個offset參數(shù),而且這個參數(shù)是會變的,所以也需要把這個參數(shù)簡單包裝成Observable, bind 在 ViewModel 的 offset 上(請注意此時的ViewModel的offset是作為Observer), 一旦View里面的offset發(fā)生了變化,ViewModel里面的offset就能接收到。
  2. 而在ViewModel里面,又將自身的 offset作為 Observable, 有變化就會去調(diào)用API,從而獲取到Observable<[NewsSections]>,通過newsData傳出去。
  3. 然后在View上又將newsData綁定在TableView的datasource上,展示拿到的數(shù)據(jù)。

也印證了這張圖:

image.png
import UIKit
import RxSwift
import RxCocoa
import RxDataSources

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var refreshItem: UIBarButtonItem!
    
    private let viewModel = NewsViewModel()
    
    private let offset = Variable("0")
    
    private let disposeBag = DisposeBag()
    
    private var dataSource: RxTableViewSectionedReloadDataSource<NewsSections>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        
        self.offset.asObservable()
            .bind(to: viewModel.offset)
            .disposed(by: disposeBag)
        
        dataSource = RxTableViewSectionedReloadDataSource<NewsSections>(configureCell: { dataSource, tableView, indexpath, item  in
            if item.imgnewextra?.isEmpty ?? true,
                let cell = tableView.dequeueReusableCell(withIdentifier: "OneImageNewsTableViewCell", for: indexpath) as? OneImageNewsTableViewCell {
                cell.setup(item)
                return cell
            } else if let cell = tableView.dequeueReusableCell(withIdentifier: "ThreeImagesTableViewCell", for: indexpath) as? ThreeImagesTableViewCell {
                cell.setup(item)
                return cell
            }
            return UITableViewCell()
        })
        
        tableView.rx.setDelegate(self)
            .disposed(by: disposeBag)
        
        viewModel.newsData
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

        refreshItem.rx.tap.bind {
            let offset = Int(self.offset.value) ?? 0
            self.offset.value = "\(offset + 10)"
        }.disposed(by: disposeBag)
        
    }
    
    private func setupTableView() {
        tableView.register(OneImageNewsTableViewCell.self, forCellReuseIdentifier: "OneImageNewsTableViewCell")
        tableView.register(ThreeImagesTableViewCell.self, forCellReuseIdentifier: "ThreeImagesTableViewCell")
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let newsSection = dataSource.sectionModels[indexPath.section]
        let news = newsSection.items[indexPath.row]
        if news.imgnewextra?.isEmpty ?? true {
            return 100.0
        }
        return 180.0
    }
}


寫在最后:

RxSwift可以說是鏈式編程的產(chǎn)物,結(jié)合Rxcocoa之后變成了可以運用在MVVM架構(gòu)上比較具有靈活性的框架。
得去掌握基本的概念之后,才能知道為什么ObservableObserver這樣用。

總的來說,不考慮嚴謹性地比喻,可以把上面demo的需求看成一個閉環(huán),看成一個生產(chǎn)行為,View就是珠寶客戶,ViewModel是珠寶雕刻的廠家, APIManager是將珠寶礦石初步加工的廠家。
Viewoffset給到ViewModel, ViewModel 把拿到的 offset給到APIManager進行網(wǎng)絡(luò)請求,拿到對應(yīng)的Model結(jié)果,再一層層給到View。

就很像 客戶 拿了張照片,告訴 珠寶雕刻的廠家 他要做照片上的玉,珠寶雕刻的廠家 做了張 設(shè)計稿 給到 珠寶礦石初步加工的廠家, 初步加工之后,再給到珠寶雕刻的廠家 進行驗收或者再次加工,再給到客戶驗收。只不過,中間不管那個角色發(fā)出了指令,下面都會立刻執(zhí)行。

Demo 地址 ?? >>>WangYiNewsRxSwiftDemo
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容