文章:

Build a simple SpaceX Launches iOS app with MVVM and RxSwift

源码

GitHub - ykpoh/SpaceXLaunch: A iOS app that shows SpaceX launch schedules based on their API. You can also view the rocket details when pressing on any of the launches.

测试case

Implement Unit Testing in a simple iOS app with MVVM and RxSwift

整体架构MVVM

代码里大量用到了 BehaviorRelay, 这是 RxSwift 里的一个变量,既可以接收(accept)也可以 转发(relay)事件(next event)。 使用关键字 accept ,可以传递一个值给对象,对象接收到值后会广播给所有订阅者。 LaunchListTableViewCell 有示例代码。

在MVVM中,需要数据(data,model,entity 无论叫啥名字)绑定 ViewModel,然后通过ViewModel 跟Controller 沟通。

RxCocoa 里的各种部件具体可以参考

RxMarbles: Interactive diagrams of Rx Observables

列表页面

源码分析

整个tableviewlist

///
// 1-5 取网络数据,赋值(accept)给 LaunchListViewModel 类的 launchViewModels// 1. 类 LaunchListViewController 列表页面
var viewModel: LaunchListViewModelType = LaunchListViewModel()// 2. 类 LaunchListViewModel
init(apiService: APIServiceProtocol = APIService()) {self.apiService = apiServicefetchLaunchesWithQuery()
}// 3. 类 LaunchListViewModel
func fetchLaunchesWithQuery() {apiService.fetchLaunchesWithQuery { [weak self] (launchResponse, error, _) inguard let strongSelf = self else { return }if let launchResponse = launchResponse {strongSelf.processFetchedLaunches(launchResponse: launchResponse)} else if let error = error {strongSelf.notifyError.accept(error)}}
}// 4. 类 LaunchListViewModel
func processFetchedLaunches(launchResponse: LaunchResponse) {guard let launches = launchResponse.docs else { return }launchViewModels.accept(convertLaunchesToLaunchListTableViewCellViewModels(launches: launches))
}// 5. LaunchListViewModel
func convertLaunchesToLaunchListTableViewCellViewModels(launches: [Launch]) -> [LaunchListTableViewCellViewModel] {var launchListTableViewCellViewModels: [LaunchListTableViewCellViewModel] = []for launch in launches {launchListTableViewCellViewModels.append(LaunchListTableViewCellViewModel(launch: launch))}return launchListTableViewCellViewModels
}//  类 LaunchListViewModel
var launchViewModels = BehaviorRelay<[LaunchListTableViewCellViewModel]>(value: [])/////  类 LaunchListViewController asDriver 类似 asObserverable , drive 类似 subscribe
//  观察 launchViewModels, 之前 1-4 有变化(accept),所以会调用reloadData
//  建立  view-viewmodel 关联
viewModel.launchViewModels.asDriver().drive(onNext: { [weak self] value in
guard let strongSelf = self else { return }
strongSelf.tableView.reloadData()}).disposed(by: disposeBag)

cell


// 1. 类 LaunchListViewController
func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {guard viewModel.launchViewModels.value.count > 0 else {return UITableViewCell()}return listingCell(tableView, indexPath)
}// 2. 类 LaunchListViewController
private func listingCell(_ tableView: UITableView, _ indexPath: IndexPath) -> LaunchListTableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier:"\(LaunchListTableViewCell.self)") as! LaunchListTableViewCelllet vm = viewModel.launchViewModels.value[indexPath.row]vm.configure(cell)return cell
}// 3. 类 LaunchListTableViewCellViewModel
public func configure(_ cell: LaunchListTableViewCell) {cell.viewModel = selfcell.setupListeners()
}// 4. 类 LaunchListTableViewCell
// 重新建立  view-viewmodel 关联
func setupListeners() {// 重新复制成员变量disposeBag, 使得之前建立的各种关联被disposed// prepareForReuse 也做了这个操作disposeBag = DisposeBag()viewModel.launchNumber.asDriver().drive(launchNumberLabel.rx.text).disposed(by: disposeBag)。。。
}

详情页面

    // 跳转到详情页 LaunchListViewControllerfunc tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {guard let rocketName = viewModel.launchViewModels.value[indexPath.row].launch.value?.rocket else { return }let rocketDetailVC = RocketDetailViewController.instanceFromStoryboard()rocketDetailVC.viewModel = RocketDetailViewModel(rocketName: rocketName)navigationController?.pushViewController(rocketDetailVC, animated: true)}// RocketDetailViewModelconvenience init(rocketName: String, apiService: APIServiceProtocol = APIService()) {self.init(apiService: apiService)fetchRocket(rocketName: rocketName)}func fetchRocket(rocketName: String) {_ = apiService.fetchRocket(rocketName: rocketName, completion: { [weak self] (rocket, error, _) inguard let strongSelf = self else { return }if let rocket = rocket {strongSelf.processFetchedRockets(rocket: rocket)} else if let error = error {strongSelf.notifyError.accept(error)}})}func processFetchedRockets(rocket: Rocket) {title.accept(rocket.name)description.accept(rocket.description)url.accept(rocket.wikipedia)imageVMs.accept({ () -> [RocketDetailImageCollectionViewCellViewModel] inreturn (rocket.flickr_images?.compactMap({ (url) -> RocketDetailImageCollectionViewCellViewModel? inreturn RocketDetailImageCollectionViewCellViewModel(imageURL: url)}) ?? [])}())self.rocket.accept(rocket)}// 3. RocketDetailViewController viewdidload 里建立 viewmodel 和 view 的关联override func viewDidLoad() {super.viewDidLoad()// Do any additional setup after loading the view.collectionView.delegate = selfcollectionView.dataSource = selfsetupCollectionViewLayout()setupListeners()}func setupListeners() {disposeBag = DisposeBag()button.rx.tap.bind(to: viewModel.buttonTapAction).disposed(by: disposeBag)viewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: disposeBag)。。。。}

RxSwift 部分解释

  • asDriver() – 代码里将 BehaviorRelay 转换为 Driver, Driver 是可观察序列(被观察者),运行在主线程上,主要用来更新UI部件
  • drive() 当值发生改变时,将会更新UI
  • dispose(by: disposeBag) 绑定到可观察者序列上,当disposeBag 被重新赋值,或者设置为nil时,将会结束序列。

单元测试

推荐文章 iOS Unit Testing and UI Testing Tutorial | Kodeco, the new raywenderlich.com

汇总5个关键点

  • 快速Fast: 单元测试能快速运行
  • 独立Independent/Isolated: 测试用例不要相互依赖
  • 可重复执行Repeatable: 每次执行要得到相同的结果
  • 自验证Self-validating: 测试必须自动化,输出是成功或者失败,不能依赖于程序员的解释
  • 时机Timely: 最理想的状态是你写代码前,先写测试用例

sample里有写好的单元测试 cmd+U执行全部测试

使用MVVM Swift UIKit RxSwift 写一个SpaceX 发射计划APP相关推荐

  1. [纯代码] Swift+UIKit · 搭建第一个iOS APP项目

    本文目录 前言 创建一个纯代码编辑的Swift + UIKit项目 创建一个Swift + UIKit项目 让它变成纯代码编辑的 让你的APP打开指定的ViewController 创建一个窗口 编辑 ...

  2. Flutter实战(一)写一个天气查询的APP

    先上效果图: 代码github地址:github.com/koudle/GDG_- 1.创建工程 在Android Studio中,File -> New ->New Flutter Pr ...

  3. 吕文翰 php,自己动手写一个 iOS 网络请求库(三)——降低耦合

    自己动手写一个 iOS 网络请求库(三)--降低耦合 2015-5-22 / 阅读数:16112 / 分类: iOS & Swift 本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实 ...

  4. 从零到一写一个完整的 Compose 版本的天气

    忍不了了 最近在手机上看天气的时候发现一堆广告,烦得要死,一个天气app,我想要的只是查看下天气,结果钢筋天气应用就给我来个开屏广告,好家伙,原来我这么喜欢广告吗???开屏广告就不说了,我忍了,因为很 ...

  5. 「中高级前端进阶」从零开始手写一个 vue-cli 脚手架

    关注我的小伙伴应该知道,我之前写过一篇脚手架相关的文章,在掘金收获了近一千个赞,被前端大全和奇舞周刊公众号转载. 可以说自定义脚手架是每一个中高级前端都应该具备的能力. 1. 脚手架带来的便利 在现在 ...

  6. [Kotlin]手把手教你写一个安卓APP(第一章注册登录)

    1.创建项目默认选择Empty Activity                                                                      点击Next ...

  7. 从零开始写一个抖音App——Apt代码生成技术、gradle插件开发与protocol协议

    1.讨论--总结前两周评论中有意义的讨论并给予我的解答 2.mvps代码生成原理--将上周的 mvps 架构的代码生成原理进行解析 3.开发一款gradle插件--从 mvps 的代码引出 gradl ...

  8. 手写一个promise用法_手写一个 Promise

    1 js 的基本数据类型? 2 JavaScript 有几种类型的值? 3 什么是堆?什么是栈?它们之间有什么区别和联系? 4 内部属性 [Class] 是什么? 5 介绍 js 有哪些内置对象? 6 ...

  9. 从零开始仿写一个抖音App

    点击上方"何俊林",马上关注,每天早上8:50准时推送 真爱,请置顶或星标 本文转载自公号开发者技术前线,原文:https://juejin.im/post/5b9e9bf1e51 ...

最新文章

  1. R语言ggplot2包旋转(Rotate)可视化图像轴标签实战
  2. AD数据采集的“数字滤波”:10个“软件滤波程序”
  3. Java 8 - Optional全解
  4. PostgreSQL索引页
  5. ❗HTML引入JavaScript的三种常用方式汇总❗
  6. asp.net导出GridView数据到Excel
  7. Centos5.5安装使用Xen
  8. s905各种型号的区别_梯式桥架和槽式桥架的区别介绍
  9. HDU - 6383 百度之星2018初赛B 1004 p1m2(二分答案)
  10. python集合的元素可以是_Python集合的元素中,为什么不可以是包含嵌套列表的元组?...
  11. 算法训练+乘法表c语言,[蓝桥杯][算法提高VIP]输出九九乘法表 (C语言代码)
  12. 资源成本双优化!看 Serverless 颠覆编程教育的创新实践
  13. 投毒、伪装、攻击,DNS 欺骗和钓鱼网站如何一步步诱人掉入陷阱?
  14. redis 系列27 Cluster高可用 (2)
  15. 云端软件平台 如何共享自己封装的云端软件
  16. Unity模拟鼠标点击
  17. 交换机怎么用计算机配置,配置交换机,教您怎么配置交换机
  18. php查找sql,sql如何去重查询
  19. Vagrant设置局域网访问
  20. search engine “DuckDuckGo”

热门文章

  1. Spring 5 详细教程 IDEA版本 复习笔记 狂神笔记 面试宝典
  2. 比特币铺就通往个体主权之路
  3. JZOJ5462. 【NOIP2017提高A组冲刺11.8】好文章
  4. HTML5实现一个时钟动画
  5. Vue使用Antv F2
  6. 股票实现自动止损止盈 股票自动止损止盈策略 python量化策略
  7. 开源免费OA教程:移动端工作表单操作条的使用方法
  8. python中装饰器修复技术_12步轻松搞定Python装饰器
  9. 运动学模型(二)----轮速计 后轮速差模型
  10. Swift中由找不到removeAll(where:)方法引起的连锁反应(下)