美文网首页ios开发那些事
【Kickstarter-iOS 源码分析】03 - MVVM

【Kickstarter-iOS 源码分析】03 - MVVM

作者: Lebron_James | 来源:发表于2019-05-10 12:05 被阅读92次

    Kickstarter-iOS 把 MVVM 模式贯彻地非常彻底。MVVM 的全称是 Model-View-ViewModel,所以我们可能会觉得要有 View 存在的地方,才可以用 ViewModel。但是 Kickstarter-iOS 在 AppDelegate 中也使用了 ViewModel,把很多在 AppDelegate 处理的逻辑剥离到 AppDelegateViewModelType 中。

    这篇文章主要讲一下 Kickstarter-iOS 在使用 MVVM 架构时用到的一些很好的技巧。

    所需知识

    默认调用 bindViewModel()

    在 MVVM 架构中,一般来说 ViewModel 是被 UIViewUIViewController 持有,而持有 ViewModel 的对象就需要绑定到 ViewModel,这样就能响应 ViewModel 中数据的变化,从而更新 UI。一般我们都会在持有 ViewModel 的对象中定义一个方法 bindViewModel(),并且在这个方法里面做绑定。

    Kickstarter 分别在 UIViewUIViewController 做了一些处理,让程序在启动的时候就默认在各自内部的方法调用了 bindViewModel(),这样可以避免在很多的 View 和 ViewController 中写重复的代码。

    UIView

    对于 UIView,Kickstarter 通过扩展重写 awakeFromNib(),在内部调用 bindViewModel()。代码如下:

    extension UIView {
      open override func awakeFromNib() {
        super.awakeFromNib()
        self.bindViewModel()
      }
    
      @objc open func bindViewModel() {
      }
    }
    

    因为 Kickstarter 在整个项目中都是通过 xib 来构建 UI 的,所以 UI 在初始化时,awakeFromNib()会被调用,从而 bindViewModel() 也被调用。那么在其他继承自 UIView 的 view 中,只需要重写 bindViewModel(),就能达到绑定 ViewModel 的目的。

    UIViewController

    UIViewController 中就会稍微复杂一点。Kickstarter 通过 runtime,默认在 viewDidLoad() 中调用 bindViewModel()。那么在其他继承自 UIViewController 的 ViewController 中,只需要重写 bindViewModel(),就能达到绑定 ViewModel 的目的。

    UIViewController-Preparation.swift相关代码如下:

    private func swizzle(_ vc: UIViewController.Type) {
    
      [
        (#selector(vc.viewDidLoad), #selector(vc.ksr_viewDidLoad)),
        (#selector(vc.viewWillAppear(_:)), #selector(vc.ksr_viewWillAppear(_:))),
        (#selector(vc.traitCollectionDidChange(_:)), #selector(vc.ksr_traitCollectionDidChange(_:))),
        ].forEach { original, swizzled in
    
          guard let originalMethod = class_getInstanceMethod(vc, original),
            let swizzledMethod = class_getInstanceMethod(vc, swizzled) else { return }
    
          let didAddViewDidLoadMethod = class_addMethod(vc,
                                                        original,
                                                        method_getImplementation(swizzledMethod),
                                                        method_getTypeEncoding(swizzledMethod))
    
          if didAddViewDidLoadMethod {
            class_replaceMethod(vc,
                                swizzled,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod))
          } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
          }
      }
    }
    
    private var hasSwizzled = false
    
    extension UIViewController {
      final public class func doBadSwizzleStuff() {
        guard !hasSwizzled else { return }
    
        hasSwizzled = true
        swizzle(self)
      }
    
      @objc internal func ksr_viewDidLoad() {
        self.ksr_viewDidLoad()
        self.bindViewModel()
      }
    
      /**
       The entry point to bind all view model outputs. Called just before `viewDidLoad`.
       */
      @objc open func bindViewModel() {
      }
    }
    

    然后在 AppDelegate.swift中的 didFinishLaunchingWithOptions调用 doBadSwizzleStuff(),代码如下:

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
        UIViewController.doBadSwizzleStuff()
    }
    

    通过这两个处理,就能避免编写大量的重复代码。

    ViewModel

    我从项目中找了一个代码量比较少的 ViewModel 文件 HelpWebViewModel.swift,以这个文件为例。具体代码如下:

    import Library
    import Prelude
    import ReactiveSwift
    import Result
    
    internal protocol HelpWebViewModelInputs {
      /// Call to configure with HelpType.
      func configureWith(helpType: HelpType)
    
      /// Call when the view loads.
      func viewDidLoad()
    }
    
    internal protocol HelpWebViewModelOutputs {
      /// Emits a request that should be loaded into the webview.
      var webViewLoadRequest: Signal<URLRequest, NoError> { get }
    }
    
    internal protocol HelpWebViewModelType {
      var inputs: HelpWebViewModelInputs { get }
      var outputs: HelpWebViewModelOutputs { get }
    }
    
    internal final class HelpWebViewModel: HelpWebViewModelType, HelpWebViewModelInputs, HelpWebViewModelOutputs {
      internal init() {
        self.webViewLoadRequest = self.helpTypeProperty.signal.skipNil()
          .takeWhen(self.viewDidLoadProperty.signal)
          .map { urlForHelpType($0, baseUrl: AppEnvironment.current.apiService.serverConfig.webBaseUrl) }
          .skipNil()
          .map { AppEnvironment.current.apiService.preparedRequest(forURL: $0) }
      }
    
      internal var inputs: HelpWebViewModelInputs { return self }
      internal var outputs: HelpWebViewModelOutputs { return self }
    
      internal let webViewLoadRequest: Signal<URLRequest, NoError>
    
      fileprivate let helpTypeProperty = MutableProperty<HelpType?>(nil)
      func configureWith(helpType: HelpType) {
        self.helpTypeProperty.value = helpType
      }
      fileprivate let viewDidLoadProperty = MutableProperty(())
      func viewDidLoad() {
        self.viewDidLoadProperty.value = ()
      }
    }
    
    private func urlForHelpType(_ helpType: HelpType, baseUrl: URL) -> URL? {
      switch helpType {
      case .cookie:
        return baseUrl.appendingPathComponent("cookies")
      case .contact:
        return nil
      case .helpCenter:
        return baseUrl.appendingPathComponent("help")
      case .howItWorks:
        return baseUrl.appendingPathComponent("about")
      case .privacy:
        return baseUrl.appendingPathComponent("privacy")
      case .terms:
        return baseUrl.appendingPathComponent("terms-of-use")
      case .trust:
        return baseUrl.appendingPathComponent("trust")
      }
    }
    

    对于 ViewModel,我想说两点:1)使用 ReactiveSwift;2)使用 inputs 和 outputs 区分数据的输入和输出。

    使用 ReactiveSwift

    在文章的开头的 所需知识 部分,我就提到过,响应式编程非常适合 MVVM 架构。在 ViewModel 中,我们通常会使用 ReactiveSwift 或者 RxSwift 去定义一些属性,然后在 UIViewUIViewController中的 bindViewModel() 方法里面订阅那些属性的变化,然后更新 UI。

    至于在开发过程中,我们该选择哪一种呢?我个人更偏向于 ReactiveSwift。大家可以看看这篇对比文章 How does ReactiveSwift relate to RxSwift?

    使用 inputs 和 outputs 区分数据的输入和输出

    在 ViewModel 中,我们需要接受外部的信息输入,并且告诉外部有哪些信息发生了变化。

    Kickstarter-iOS 把信息的输入和输出分别用 HelpWebViewModelInputsHelpWebViewModelOutputs 分开,这样在使用 ViewModel 的时候就会非常清晰,不会把 inputs 和 outputs 混在一起。例如,我们在Xcode 中编写 viewModel.outputs. 时,Xcode 只会提示 webViewLoadRequest,而不会把属于 inputsviewDidLoad()也显示给我们。

    这在我们使用 ViewModel 的时候带来了极大的便利,并且让看代码的人一目了然,哪些代码处理输入,哪些代码处理输出,非常清晰。

    这里有一个讲解 MVVM & TDD 的视频,大家可以去看一下,需要自带梯子。

    想及时看到我的新文章的,可以关注我。同时也欢迎加入我管理的Swift开发群:536353151

    下一篇文章:【Kickstarter-iOS 源码分析】04 - Environment 和 AppEnvironment

    相关文章

      网友评论

        本文标题:【Kickstarter-iOS 源码分析】03 - MVVM

        本文链接:https://www.haomeiwen.com/subject/ywoooqtx.html