使用MVVM Swift UIKit RxSwift 写一个SpaceX 发射计划APP
文章:
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相关推荐
- [纯代码] Swift+UIKit · 搭建第一个iOS APP项目
本文目录 前言 创建一个纯代码编辑的Swift + UIKit项目 创建一个Swift + UIKit项目 让它变成纯代码编辑的 让你的APP打开指定的ViewController 创建一个窗口 编辑 ...
- Flutter实战(一)写一个天气查询的APP
先上效果图: 代码github地址:github.com/koudle/GDG_- 1.创建工程 在Android Studio中,File -> New ->New Flutter Pr ...
- 吕文翰 php,自己动手写一个 iOS 网络请求库(三)——降低耦合
自己动手写一个 iOS 网络请求库(三)--降低耦合 2015-5-22 / 阅读数:16112 / 分类: iOS & Swift 本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实 ...
- 从零到一写一个完整的 Compose 版本的天气
忍不了了 最近在手机上看天气的时候发现一堆广告,烦得要死,一个天气app,我想要的只是查看下天气,结果钢筋天气应用就给我来个开屏广告,好家伙,原来我这么喜欢广告吗???开屏广告就不说了,我忍了,因为很 ...
- 「中高级前端进阶」从零开始手写一个 vue-cli 脚手架
关注我的小伙伴应该知道,我之前写过一篇脚手架相关的文章,在掘金收获了近一千个赞,被前端大全和奇舞周刊公众号转载. 可以说自定义脚手架是每一个中高级前端都应该具备的能力. 1. 脚手架带来的便利 在现在 ...
- [Kotlin]手把手教你写一个安卓APP(第一章注册登录)
1.创建项目默认选择Empty Activity 点击Next ...
- 从零开始写一个抖音App——Apt代码生成技术、gradle插件开发与protocol协议
1.讨论--总结前两周评论中有意义的讨论并给予我的解答 2.mvps代码生成原理--将上周的 mvps 架构的代码生成原理进行解析 3.开发一款gradle插件--从 mvps 的代码引出 gradl ...
- 手写一个promise用法_手写一个 Promise
1 js 的基本数据类型? 2 JavaScript 有几种类型的值? 3 什么是堆?什么是栈?它们之间有什么区别和联系? 4 内部属性 [Class] 是什么? 5 介绍 js 有哪些内置对象? 6 ...
- 从零开始仿写一个抖音App
点击上方"何俊林",马上关注,每天早上8:50准时推送 真爱,请置顶或星标 本文转载自公号开发者技术前线,原文:https://juejin.im/post/5b9e9bf1e51 ...
最新文章
- R语言ggplot2包旋转(Rotate)可视化图像轴标签实战
- AD数据采集的“数字滤波”:10个“软件滤波程序”
- Java 8 - Optional全解
- PostgreSQL索引页
- ❗HTML引入JavaScript的三种常用方式汇总❗
- asp.net导出GridView数据到Excel
- Centos5.5安装使用Xen
- s905各种型号的区别_梯式桥架和槽式桥架的区别介绍
- HDU - 6383 百度之星2018初赛B 1004 p1m2(二分答案)
- python集合的元素可以是_Python集合的元素中,为什么不可以是包含嵌套列表的元组?...
- 算法训练+乘法表c语言,[蓝桥杯][算法提高VIP]输出九九乘法表 (C语言代码)
- 资源成本双优化!看 Serverless 颠覆编程教育的创新实践
- 投毒、伪装、攻击,DNS 欺骗和钓鱼网站如何一步步诱人掉入陷阱?
- redis 系列27 Cluster高可用 (2)
- 云端软件平台 如何共享自己封装的云端软件
- Unity模拟鼠标点击
- 交换机怎么用计算机配置,配置交换机,教您怎么配置交换机
- php查找sql,sql如何去重查询
- Vagrant设置局域网访问
- search engine “DuckDuckGo”
热门文章
- Spring 5 详细教程 IDEA版本 复习笔记 狂神笔记 面试宝典
- 比特币铺就通往个体主权之路
- JZOJ5462. 【NOIP2017提高A组冲刺11.8】好文章
- HTML5实现一个时钟动画
- Vue使用Antv F2
- 股票实现自动止损止盈 股票自动止损止盈策略 python量化策略
- 开源免费OA教程:移动端工作表单操作条的使用方法
- python中装饰器修复技术_12步轻松搞定Python装饰器
- 运动学模型(二)----轮速计 后轮速差模型
- Swift中由找不到removeAll(where:)方法引起的连锁反应(下)