请稍侯

iOS中的MVC、MVVM 研究

17 June 2016

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