iOS中的MVC、MVVM 研究

iOS中的MVC、MVVM 研究

  • 一个标准的MVC架构

    • Model

        @interface Person : NSObject
      
        - (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
      
        @property (nonatomic, readonly) NSString *salutation;
        @property (nonatomic, readonly) NSString *firstName;
        @property (nonatomic, readonly) NSString *lastName;
        @property (nonatomic, readonly) NSDate *birthdate;
      
        @end
      
    • ViewController,有一个 PersonViewController ,在 viewDidLoad 里,只需要基于它的 model 属性设置一些 Label 即可

        - (void)viewDidLoad {
           [super viewDidLoad];
      
           if (self.model.salutation.length > 0) {
               self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
           } else {
               self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
           }
      
           NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
           [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
           self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
        }
      
  • 从MVC 到 MVVM 的演化

    • 优点

        - MVVM 可以兼容你当下使用的 MVC 架构;
        - MVVM 增加你的应用的可测试性;
        MVVM 配合一个绑定机制效果最好。
      
    • 缺点

        - 学习成本和开发成本都很高,新人很难上手;
        -  iOS中,并没有现成的绑定机制可用,要么使用 KVO,要么引入类似 ReactiveCocoa 这样的第三方库;
        -  数据绑定使 Debug 变得更难了,堆栈结构更复杂了,使得对象生命周期很难追踪;
        -  对于过大的项目,数据绑定需要花费更多的内存;
        -  ReactiveCocoa 在国内外还都是在小众领域,没有被大量接受成为主流的编程框架。在别的语言中,例如 Java 中的 RxJava 也同样没有成为主流;
      
    • 用一个 View Model 来增强它

        @interface PersonViewModel : NSObject
      
        - (instancetype)initWithPerson:(Person *)person;
      
        @property (nonatomic, readonly) Person *person;
      
        @property (nonatomic, readonly) NSString *nameText;
        @property (nonatomic, readonly) NSString *birthdateText;
      
        @end
      

      View Model 的实现大概如下:

        @implementation PersonViewModel
      
        - (instancetype)initWithPerson:(Person *)person {
            self = [super init];
            if (!self) return nil;
      
            _person = person;
            if (person.salutation.length > 0) {
                _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
            } else {
                _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
            }
      
            NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
            [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
            _birthdateText = [dateFormatter stringFromDate:person.birthdate];
      
            return self;
        }
      
        @end
      
    • 将 viewDidLoad 中的表示逻辑放入我们的 View Model 里了。此时,我们新的 viewDidLoad 就会非常轻量:

        - (void)viewDidLoad {
            [super viewDidLoad];
      
            self.nameLabel.text = self.viewModel.nameText;
            self.birthdateLabel.text = self.viewModel.birthdateText;
        }
      
    • 在这个简单的例子中, Model 是不可变的,所以我们可以只在初始化的时候指定我们 View Model 的属性。对于可变 Model,我们还需要使用一些绑定机制,这样 View Model 就能在背后的 Model 改变时更新自身的属性。此外,一旦 View Model 上的 Model 发生改变,那 View 的属性也需要更新。Model 的改变应该级联向下通过 View Model 进入 View。

    • 在 OS X 上,我们可以使用 Cocoa 绑定,但在 iOS 上我们并没有这样好的配置可用。所以在iOS上一般可以通过 KVO 或 ReactiveCocoa 实现数据绑定;

  • iOS 中 MVVM 的改进方案

    1. 对 ViewModel 不引入双向绑定机制或者观察机制,而是通过传统的代理回调或是通知来将 UI 事件传递给外界。这样有3个好处:

      • 首先是 View 的完全解耦合,只需要确定好相应的 ViewModel 和 UI 事件的回调接口即可与 Model 层完全隔离;
      • ViewController 可以避免与 View 的具体表现打交道,这部分职责被转交给了 ViewModel,有效的减轻了 ViewController 的负担;
      • 同时我们弃用了传统绑定机制,使用了传统的易于理解的回调机制来传递 UI 事件,降低了学习成本,同时使得数据的流入和流出变得易于观察和控制,降低了维护了调适的成本。
    2. 对每一个 ViewController 都创建一个对应的 DataController,代码如下:

       @interface APEHomePracticeViewController () <APEHomePracticeSubjectsViewDelegate>   
       @property (nonatomic, strong, nullable) UIScrollView *contentView;
       @property (nonatomic, strong, nullable) APEHomePracticeBannerView *bannerView;
       @property (nonatomic, strong, nullable) APEHomePracticeActivityView *activityView;
       @property (nonatomic, strong, nullable) APEHomePracticeSubjectsView *subjectsView;
      
       @property (nonatomic, strong, nullable) APEHomePracticeDataController *dataController;
      
       @end
      
    3. 在 viewDidLoad 的时候,初始化好各个 SubView,并设置好布局:

       - (void)setupContentView {
           self.contentView = [[UIScrollView alloc] init];
           [self.view addSubview:self.contentView];
      
           self.bannerView = [[APEHomePracticeBannerView alloc] init];
           self.activityView = [[APEHomePracticeActivityView alloc] init];
           self.subjectsView = [[APEHomePracticeSubjectsView alloc] init];
           self.subjectsView.delegate = self;
      
           [self.contentView addSubview:self.bannerView];
           [self.contentView addSubview:self.activityView];
           [self.contentView addSubview:self.subjectsView];
           // Layout Views ...
       }
      
    4. 接下来,ViewController 会向 DataController 请求 Subject 相关的数据,并在请求完成后,用获得的数据生成 ViewModel,将其装配给 SubjectView,完成界面渲染,代码如下:

       - (void)fetchSubjectData {
           [self.dataController requestSubjectDataWithCallback:^(NSError *error) {
               if (error == nil) {
                   [self renderSubjectView];
             }
         }];
       }
       - (void)renderSubjectView {
           APEHomePracticeSubjectsViewModel *viewModel =
               [APEHomePracticeSubjectsViewModel viewModelWithSubjects:self.dataController.openSubjects];
           [self.subjectsView bindDataWithViewModel:viewModel];
       }
      
    5. DataController,每一个 ViewController 都会有一个对应的 DataController,这一类 DataController 的主要职责是处理这个页面上的所有数据相关的逻辑,我们称其为 View Related Data Controller。

       // APEHomePracticeDataController.h
       @interface APEHomePracticeDataController : APEBaseDataController
       // 1
       @property (nonatomic, strong, nonnull, readonly) NSArray<APESubject *> *openSubjects;
       // 2
       - (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback;
      
       @end
      
    6. DataController 这一层是一个灵活性很高的部件,一个 DataController 可以复用更小的 DataController,这一类更小的 DataController 通常只会包含纯粹的或是更抽象的 Model 相关的逻辑,例如网络请求,数据库请求,或是数据加工等。我们称这一类 DataController 为 Model Related Data Controller。

      Model Related Data Controller(以下为subjectDataController)通常会为上层提供正交的数据:

       // APEHomePracticeDataController.m
       @interface APEHomePracticeDataController ()
      
       @property (nonatomic, strong, nonnull) APESubjectDataController *subjectDataController;
      
       @end
      
       @implementation APEHomePracticeDataController
      
       - (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback {
           APEDataCallback dataCallback = ^(NSError *error, id data) {
               callback(error);
           };
           [self.subjectDataController requestAllSubjectsWithCallback:dataCallback];
           [self.subjectDataController requestUserSubjectsWithCallback:dataCallback];
       }
      
       - (nonnull NSArray<APESubject *> *)openSubjects {
           return self.subjectDataController.openSubjectsWithCurrentPhase ?: @[];
       }
      
       @end
      
  • Swift版的基于KVO 的 MVVM案例

      import UIKit
    
      struct Person { // Model
          let firstName: String
          let lastName: String
      }
    
      protocol GreetingViewModelProtocol: class {
          var greeting: String? { get }
          var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
          init(person: Person)
          func showGreeting()
      }
    
      class GreetingViewModel : GreetingViewModelProtocol {
          let person: Person
          var greeting: String? {
              didSet {
                  self.greetingDidChange?(self)
              }
          }
          var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
          required init(person: Person) {
              self.person = person
          }
          func showGreeting() {
              self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
          }
      }
    
      class GreetingViewController : UIViewController {
          var viewModel: GreetingViewModelProtocol! {
              didSet {
                  self.viewModel.greetingDidChange = { [unowned self] viewModel in
                      self.greetingLabel.text = viewModel.greeting
                  }
              }
          }
          let showGreetingButton = UIButton()
          let greetingLabel = UILabel()
    
          override func viewDidLoad() {
              super.viewDidLoad()
              self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
          }
          // layout code goes here
      }
      // Assembling of MVVM
      let model = Person(firstName: "David", lastName: "Blaine")
      let viewModel = GreetingViewModel(person: model)
      let view = GreetingViewController()
      view.viewModel = viewModel
    

以上参考:

MVVM 介绍

iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER

猿题库 iOS 客户端架构设计

被误解的MVC和被神化的MVVM

{{ message }}

{{ 'Comments are closed.' | trans }}