RxSwift & MVVM

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容