RxSwift & MVVM

作者: Grabin | 来源:发表于2019-04-28 18:06 被阅读0次
image.png

简单说下在MVVM架构下使用RxSwift的思路:
ViewController在这个架构中,也是属于View这个层级。

首先,假设需要搭建一个UI界面,而且这个页面需要向API发送请求,获取数据来展示UI界面。

  1. View 会把需要的请求参数以observable的形式让 ViewModel 接收到(不管是绑定还是订阅)
  2. ViewModel 从接收到的observable拿到所需要的数据,调用 APIManager, 获取到类型 Modelobservable,也就是observable<Model> 或者 observable<[Model]>
  3. ViewModel 把对应类型 Modelobservable输出给 View 使用

可以看到 ViewModel 在整个过程中,将对应的参数,通过网络请求以及数据转换,输出 View 需要的 Model 类型数据。


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

拿了网易新闻的API 来做这个Demo.gif
介绍一些用到的第三方库
target 'WangYiNews' do
  use_frameworks!
  pod 'RxSwift'
  pod 'RxCocoa'
  pod 'SnapKit'         # 跟Masonry一样是用来设置约束的,swift版
  pod 'SwiftyJSON'      # Json数据转换
  pod 'Alamofire'       # 用于网络请求
  pod 'Moya/RxSwift'    # 用于网络请求
  pod 'Kingfisher'      # SDWebImage swift 版
  pod 'RxDataSources', '~> 3.0'  # RxSwift中用于设置UITableView/UICollectionView data sources
end
文件分类.png

》》代码《《

Model 设计:

很简单,demo页面只需要这些, imgnewextra 数组是用来存储三图的情况

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 设计:

API请求只需要一个offset的参数,用于获取offset参数之后10条新闻, 所以input只需要一个Variable, output对于这个页面来说,只是需要一个model数组,用于展示新闻列表。

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,通过网络请求和数据转换变成 driver<[NewsSections]>的代码,是不可能一步到位的,此处只是附上最终调用APIManager代码之后的完整代码。
所以,知道input能拿到什么之后,就可以去设计APIManager, 也就是网络层。


APIManager(网络层) 设计:

网络层使用了 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参数,而且这个参数是会变的,所以也需要把这个参数简单包装成Observable, bind 在 ViewModel 的 offset 上(请注意此时的ViewModel的offset是作为Observer), 一旦View里面的offset发生了变化,ViewModel里面的offset就能接收到。
  2. 而在ViewModel里面,又将自身的 offset作为 Observable, 有变化就会去调用API,从而获取到Observable<[NewsSections]>,通过newsData传出去。
  3. 然后在View上又将newsData绑定在TableView的datasource上,展示拿到的数据。

也印证了这张图:

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可以说是链式编程的产物,结合Rxcocoa之后变成了可以运用在MVVM架构上比较具有灵活性的框架。
得去掌握基本的概念之后,才能知道为什么ObservableObserver这样用。

总的来说,不考虑严谨性地比喻,可以把上面demo的需求看成一个闭环,看成一个生产行为,View就是珠宝客户,ViewModel是珠宝雕刻的厂家, APIManager是将珠宝矿石初步加工的厂家。
Viewoffset给到ViewModel, ViewModel 把拿到的 offset给到APIManager进行网络请求,拿到对应的Model结果,再一层层给到View

就很像 客户 拿了张照片,告诉 珠宝雕刻的厂家 他要做照片上的玉,珠宝雕刻的厂家 做了张 设计稿 给到 珠宝矿石初步加工的厂家, 初步加工之后,再给到珠宝雕刻的厂家 进行验收或者再次加工,再给到客户验收。只不过,中间不管那个角色发出了指令,下面都会立刻执行。

Demo 地址 👉 >>>WangYiNewsRxSwiftDemo

相关文章

网友评论

    本文标题:RxSwift & MVVM

    本文链接:https://www.haomeiwen.com/subject/yegdnqtx.html