文章:
Build a simple SpaceX Launches iOS app with MVVM and RxSwift
源码
测试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

列表页面

- ///
- // 1-5 取网络数据,赋值(accept)给 LaunchListViewModel 类的 launchViewModels
-
-
- // 1. 类 LaunchListViewController 列表页面
- var viewModel: LaunchListViewModelType = LaunchListViewModel()
-
-
- // 2. 类 LaunchListViewModel
- init(apiService: APIServiceProtocol = APIService()) {
- self.apiService = apiService
- fetchLaunchesWithQuery()
- }
-
-
- // 3. 类 LaunchListViewModel
- func fetchLaunchesWithQuery() {
- apiService.fetchLaunchesWithQuery { [weak self] (launchResponse, error, _) in
- guard 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)
-
- // 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! LaunchListTableViewCell
- let vm = viewModel.launchViewModels.value[indexPath.row]
- vm.configure(cell)
- return cell
- }
-
-
-
- // 3. 类 LaunchListTableViewCellViewModel
- public func configure(_ cell: LaunchListTableViewCell) {
- cell.viewModel = self
- cell.setupListeners()
- }
-
- // 4. 类 LaunchListTableViewCell
- // 重新建立 view-viewmodel 关联
- func setupListeners() {
- // 重新复制成员变量disposeBag, 使得之前建立的各种关联被disposed
- // prepareForReuse 也做了这个操作
- disposeBag = DisposeBag()
-
- viewModel.launchNumber
- .asDriver()
- .drive(launchNumberLabel.rx.text)
- .disposed(by: disposeBag)
- 。。。
- }

-
- // 跳转到详情页 LaunchListViewController
-
- func 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)
- }
-
-
-
- // RocketDetailViewModel
- convenience 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, _) in
- guard 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] in
- return (rocket.flickr_images?.compactMap({ (url) -> RocketDetailImageCollectionViewCellViewModel? in
- return 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 = self
- collectionView.dataSource = self
-
- setupCollectionViewLayout()
- 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)
-
- 。。。。
- }
推荐文章 iOS Unit Testing and UI Testing Tutorial | Kodeco, the new raywenderlich.com
汇总5个关键点
- 快速Fast: 单元测试能快速运行
- 独立Independent/Isolated: 测试用例不要相互依赖
- 可重复执行Repeatable: 每次执行要得到相同的结果
- 自验证Self-validating: 测试必须自动化,输出是成功或者失败,不能依赖于程序员的解释
- 时机Timely: 最理想的状态是你写代码前,先写测试用例
sample里有写好的单元测试
cmd+U执行全部测试
