• 使用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. ///
    2. // 1-5 取网络数据,赋值(accept)给 LaunchListViewModel 类的 launchViewModels
    3. // 1. 类 LaunchListViewController 列表页面
    4. var viewModel: LaunchListViewModelType = LaunchListViewModel()
    5. // 2. 类 LaunchListViewModel
    6. init(apiService: APIServiceProtocol = APIService()) {
    7. self.apiService = apiService
    8. fetchLaunchesWithQuery()
    9. }
    10. // 3. 类 LaunchListViewModel
    11. func fetchLaunchesWithQuery() {
    12. apiService.fetchLaunchesWithQuery { [weak self] (launchResponse, error, _) in
    13. guard let strongSelf = self else { return }
    14. if let launchResponse = launchResponse {
    15. strongSelf.processFetchedLaunches(launchResponse: launchResponse)
    16. } else if let error = error {
    17. strongSelf.notifyError.accept(error)
    18. }
    19. }
    20. }
    21. // 4. 类 LaunchListViewModel
    22. func processFetchedLaunches(launchResponse: LaunchResponse) {
    23. guard let launches = launchResponse.docs else { return }
    24. launchViewModels.accept(convertLaunchesToLaunchListTableViewCellViewModels(launches: launches))
    25. }
    26. // 5. LaunchListViewModel
    27. func convertLaunchesToLaunchListTableViewCellViewModels(launches: [Launch]) -> [LaunchListTableViewCellViewModel] {
    28. var launchListTableViewCellViewModels: [LaunchListTableViewCellViewModel] = []
    29. for launch in launches {
    30. launchListTableViewCellViewModels.append(LaunchListTableViewCellViewModel(launch: launch))
    31. }
    32. return launchListTableViewCellViewModels
    33. }
    34. // 类 LaunchListViewModel
    35. var launchViewModels = BehaviorRelay<[LaunchListTableViewCellViewModel]>(value: [])
    36. ///
    37. // 类 LaunchListViewController asDriver 类似 asObserverable , drive 类似 subscribe
    38. // 观察 launchViewModels, 之前 1-4 有变化(accept),所以会调用reloadData
    39. // 建立 view-viewmodel 关联
    40. viewModel.launchViewModels
    41. .asDriver()
    42. .drive(onNext: { [weak self] value in
    43. guard let strongSelf = self else { return }
    44. strongSelf.tableView.reloadData()
    45. })
    46. .disposed(by: disposeBag)

    cell

    1. // 1. 类 LaunchListViewController
    2. func tableView(_ tableView: UITableView,
    3. cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    4. guard viewModel.launchViewModels.value.count > 0 else {
    5. return UITableViewCell()
    6. }
    7. return listingCell(tableView, indexPath)
    8. }
    9. // 2. 类 LaunchListViewController
    10. private func listingCell(_ tableView: UITableView, _ indexPath: IndexPath) -> LaunchListTableViewCell {
    11. let cell = tableView.dequeueReusableCell(withIdentifier:
    12. "\(LaunchListTableViewCell.self)") as! LaunchListTableViewCell
    13. let vm = viewModel.launchViewModels.value[indexPath.row]
    14. vm.configure(cell)
    15. return cell
    16. }
    17. // 3. 类 LaunchListTableViewCellViewModel
    18. public func configure(_ cell: LaunchListTableViewCell) {
    19. cell.viewModel = self
    20. cell.setupListeners()
    21. }
    22. // 4. 类 LaunchListTableViewCell
    23. // 重新建立 view-viewmodel 关联
    24. func setupListeners() {
    25. // 重新复制成员变量disposeBag, 使得之前建立的各种关联被disposed
    26. // prepareForReuse 也做了这个操作
    27. disposeBag = DisposeBag()
    28. viewModel.launchNumber
    29. .asDriver()
    30. .drive(launchNumberLabel.rx.text)
    31. .disposed(by: disposeBag)
    32. 。。。
    33. }

    详情页面

    1. // 跳转到详情页 LaunchListViewController
    2. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    3. guard let rocketName = viewModel.launchViewModels.value[indexPath.row].launch.value?.rocket else { return }
    4. let rocketDetailVC = RocketDetailViewController.instanceFromStoryboard()
    5. rocketDetailVC.viewModel = RocketDetailViewModel(rocketName: rocketName)
    6. navigationController?.pushViewController(rocketDetailVC, animated: true)
    7. }
    8. // RocketDetailViewModel
    9. convenience init(rocketName: String, apiService: APIServiceProtocol = APIService()) {
    10. self.init(apiService: apiService)
    11. fetchRocket(rocketName: rocketName)
    12. }
    13. func fetchRocket(rocketName: String) {
    14. _ = apiService.fetchRocket(rocketName: rocketName, completion: { [weak self] (rocket, error, _) in
    15. guard let strongSelf = self else { return }
    16. if let rocket = rocket {
    17. strongSelf.processFetchedRockets(rocket: rocket)
    18. } else if let error = error {
    19. strongSelf.notifyError.accept(error)
    20. }
    21. })
    22. }
    23. func processFetchedRockets(rocket: Rocket) {
    24. title.accept(rocket.name)
    25. description.accept(rocket.description)
    26. url.accept(rocket.wikipedia)
    27. imageVMs.accept({ () -> [RocketDetailImageCollectionViewCellViewModel] in
    28. return (rocket.flickr_images?.compactMap({ (url) -> RocketDetailImageCollectionViewCellViewModel? in
    29. return RocketDetailImageCollectionViewCellViewModel(imageURL: url)
    30. }) ?? [])
    31. }())
    32. self.rocket.accept(rocket)
    33. }
    34. // 3. RocketDetailViewController viewdidload 里建立 viewmodel 和 view 的关联
    35. override func viewDidLoad() {
    36. super.viewDidLoad()
    37. // Do any additional setup after loading the view.
    38. collectionView.delegate = self
    39. collectionView.dataSource = self
    40. setupCollectionViewLayout()
    41. setupListeners()
    42. }
    43. func setupListeners() {
    44. disposeBag = DisposeBag()
    45. button.rx.tap
    46. .bind(to: viewModel.buttonTapAction)
    47. .disposed(by: disposeBag)
    48. viewModel.title
    49. .asDriver()
    50. .drive(titleLabel.rx.text)
    51. .disposed(by: disposeBag)
    52. 。。。。
    53. }

    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执行全部测试

     

     

  • 相关阅读:
    sealos踩坑记录
    多机分布式执行异步任务的实现姿势
    JavaScript高阶班之ES6 → ES11(八)
    【LeetCode刷题-数组】--27.移除元素
    Java基础语法部分
    【无标题】
    Metis安装(5.0.1与4.0.3)
    Neutron — API Service Web 开发框架
    tomcat常见漏洞
    AIGC独角兽官宣联手,支持千亿大模型的云实例发布,“云计算春晚”比世界杯还热闹...
  • 原文地址:https://blog.csdn.net/linzhiji/article/details/128135925