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 的改进方案
- 对 ViewModel 不引入双向绑定机制或者观察机制,而是通过传统的代理回调或是通知来将 UI 事件传递给外界。这样有3个好处:
- 首先是 View 的完全解耦合,只需要确定好相应的 ViewModel 和 UI 事件的回调接口即可与 Model 层完全隔离;
- ViewController 可以避免与 View 的具体表现打交道,这部分职责被转交给了 ViewModel,有效的减轻了 ViewController 的负担;
- 同时我们弃用了传统绑定机制,使用了传统的易于理解的回调机制来传递 UI 事件,降低了学习成本,同时使得数据的流入和流出变得易于观察和控制,降低了维护了调适的成本。
- 对每一个 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
- 在 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 ... }
- 接下来,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]; }
- 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
- 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
- 对 ViewModel 不引入双向绑定机制或者观察机制,而是通过传统的代理回调或是通知来将 UI 事件传递给外界。这样有3个好处:
-
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
以上参考:
iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER