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
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
以上参考: