1、MVC
从字面意思来理解,MVC 即 Modal View Controller(模型 视图 控制器),是 Xerox PARC 在 20 世纪 80 年代为编程语言 Smalltalk-80 发明的一种软件设计模式,至今已广泛应用于用户交互应用程序中。其用意在于将数据与视图分离开来。在 iOS 开发中 MVC 的机制被使用的淋漓尽致,充分理解 iOS 的 MVC 模式,有助于我们程序的组织合理性。
- MVC 的几个明显的特征和体现:
- View 上面显示什么东西,取决于 Model。
- 只要 Model 数据改了,View 的显示状态会跟着更改。
Control 负责初始化 Model,并将 Model 传递给 View 去解析展示。
1)Modal 模型对象:
- 模型对象封装了应用程序的数据,并定义操控和处理该数据的逻辑和运算。例如,模型对象可能是表示商品数据 list。用户在视图层中所进行的创建或修改数据的操作,通过控制器对象传达出去,最终会创建或更新模型对象。模型对象更改时(例如通过网络连接接收到新数据),它通知控制器对象,控制器对象更新相应的视图对象。
2)View 视图对象:
视图对象是应用程序中用户可以看见的对象。视图对象知道如何将自己绘制出来,可能对用户的操作作出响应。视图对象的主要目的就是显示来自应用程序模型对象的数据,并使该数据可被编辑。尽管如此,在 MVC 应用程序中,视图对象通常与模型对象分离。
在iOS应用程序开发中,所有的控件、窗口等都继承自 UIView,对应 MVC 中的 V。UIView 及其子类主要负责 UI 的实现,而 UIView 所产生的事件都可以采用委托的方式,交给 UIViewController 实现。
3)Controller 控制器对象:
在应用程序的一个或多个视图对象和一个或多个模型对象之间,控制器对象充当媒介。控制器对象因此是同步管道程序,通过它,视图对象了解模型对象的更改,反之亦然。控制器对象还可以为应用程序执行设置和协调任务,并管理其他对象的生命周期。
控制器对象解释在视图对象中进行的用户操作,并将新的或更改过的数据传达给模型对象。模型对象更改时,一个控制器对象会将新的模型数据传达给视图对象,以便视图对象可以显示它。
对于不同的 UIView,有相应的 UIViewController,对应 MVC 中的 C。例如在 iOS 上常用的 UITableView,它所对应的 Controller 就是UITableViewController。
1.1 简单的 MVC
- 控制器加载模型数据并将数据转换为数据模型。
控制器创建视图控件,并将模型数据传递给视图控件
1.2 iOS MVC 示意图
1)Model 和 View 永远不能相互通信,只能通过 Controller 传递。
2)Controller 可以直接与 Model 对话(读写调用 Model),Model 通过 Notification 和 KVO 机制与 Controller 间接通信。
3)Controller 可以直接与 View 对话,通过 outlet,直接操作 View,outlet 直接对应到 View 中的控件,View 通过 action 向 Controller 报告事件的发生(如用户 Touch 我了)。Controller 是 View 的直接数据源(数据很可能是 Controller 从 Model 中取得并经过加工了)。Controller 是 View 的代理(delegate),以同步 View 与 Controller。
1.3 苹果推荐的 MVC -- 愿景
Cocoa MVC
由于 Controller 是一个介于 View 和 Model 之间的协调器,所以 View 和 Model 之间没有任何直接的联系。Controller 是一个最小可重用单元,这对我们来说是一个好消息,因为我们总要找一个地方来写逻辑复杂度较高的代码,而这些代码又不适合放在 Model 中。
理论上来讲,这种模式看起来非常直观,但你有没有感到哪里有一丝诡异?你甚至听说过,有人将 MVC 的缩写展开成 (Massive View Controller),更有甚者,为 View controller 减负也成为 iOS 开发者面临的一个重要话题。如果苹果继承并且对 MVC 模式有一些进展,所有这些为什么还会发生?
1.4 苹果推荐的 MVC -- 事实
Realistic Cocoa MVC
Cocoa 的 MVC 模式驱使人们写出臃肿的视图控制器,因为它们经常被混杂到 View 的生命周期中,因此很难说 View 和 ViewController 是分离的。尽管仍可以将业务逻辑和数据转换到 Model,但是大多数情况下当需要为 View 减负的时候我们却无能为力了,View 的最大的任务就是向 Controller 传递用户动作事件。ViewController 不再承担一切代理和数据源的职责,通常只负责一些分发和取消网络请求以及一些其他的任务。
你可能会看见过很多次这样的代码:
BookModel *bookModel = [myDataArray objectAtIndex:indexPath.row]; [cell configWithModel:bookModel];
- 这个 cell,正是由 View 直接来调用 Model,所以事实上 MVC 的原则已经违背了,但是这种情况是一直发生的甚至于人们不觉得这里有哪些不对。如果严格遵守 MVC 的话,你会把对 cell 的设置放在 Controller 中,不向 View 传递一个 Model 对象,这样就会大大减少 Controller 的体积。Cocoa 的 MVC 被写成 Massive View Controller 是不无道理的。
直到进行单元测试的时候才会发现问题越来越明显。因为你的 ViewController 和 View 是紧密耦合的,对它们进行测试就显得很艰难--你得有足够的创造性来模拟 View 和它们的生命周期,在以这样的方式来写 View Controller 的同时,业务逻辑的代码也逐渐被分散到 View 的布局代码中去。
1.5 MVC 自身的不足
MVC 是一个用来组织代码的权威范式,也是构建 iOS App 的标准模式。Apple 甚至是这么说的。在 MVC 下,所有的对象被归类为一个 model,一个 view,或一个 controller。Model 持有数据,View 显示与用户交互的界面,而 View Controller 调解 Model 和 View 之间的交互。然而,随着模块的迭代我们越来越发现 MVC 自身存在着很多不足。
1)MVC 在现实应用中的不足:
- 在 MVC 模式中 view 将用户交互通知给控制器。view 的控制器通过更新 Model 来反应状态的改变。Model(通常使用 Key-Value-Observation)通知控制器来更新他们负责的 view。大多数 iOS 应用程序的代码使用这种方式来组织。
2)愈发笨重的 Controller:
在传统的 app 中模型数据一般都很简单,不涉及到复杂的业务数据逻辑处理,客户端开发受限于它自身运行的的平台终端,这一点注定使移动端不像 PC 前端那样能够处理大量的复杂的业务场景。然而随着移动平台的各种深入,我们不得不考虑这个问题。传统的 Model 数据大多来源于网络数据,拿到网络数据后客户端要做的事情就是将数据直接按照顺序画在界面上。随着业务的越来越来的深入,我们依赖的 service 服务可能在大多时间无法第一时间满足客户端需要的数据需求,移动端愈发的要自行处理一部分逻辑计算操作。这个时间一惯的做法是在控制器中处理,最终导致了控制器成了垃圾箱,越来越不可维护。
控制器 Controller 是 app 的 “胶水代码”,协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的 loading、appearing、disappearing 等等,同时往往也会充满我们不愿暴露的 Model 的模型逻辑以及不愿暴露给视图的业务逻辑。这引出了第一个关于 MVC 的问题...
视图 view 通常是 UIKit 控件(component,这里根据习惯译为控件)或者编码定义的 UIKit 控件的集合。进入 .xib 或者 Storyboard 会发现一个 app、Button、Label 都是由这些可视化的和可交互的控件组成。View 不应该直接引用 Model,并且仅仅通过 IBAction 事件引用 controller。业务逻辑很明显不归入 view,视图本身没有任何业务。
厚重的 View Controller 由于大量的代码被放进 viewcontroller,导致他们变的相当臃肿。在 iOS 中有的 view controller 里绵延成千上万行代码的事并不是前所未见的。这些超重 app 的突出情况包括:厚重的 View Controller 很难维护(由于其庞大的规模);包含几十个属性,使他们的状态难以管理;遵循许多协议(protocol),导致协议的响应代码和 controller 的逻辑代码混淆在一起。
厚重的 view controller 很难测试,不管是手动测试或是使用单元测试,因为有太多可能的状态。将代码分解成更小的多个模块通常是件好事。
3)太过于轻量级的 Model:
- 早期的 Model 层,其实就是如果数据有几个属性,就定义几个属性,ARC 普及以后我们在 Model 层的实现文件中基本上看不到代码(无需再手动管理释放变量,Model 既没有复杂的业务处理,也没有对象的构造,基本上 .m 文件中的代码普遍是空的);同时与控制器的代码越来厚重形成强烈的反差,这一度让人不禁对现有的开发设计构思有所怀疑。
4)遗失的网络逻辑:
苹果使用的 MVC 的定义是这么说的:所有的对象都可以被归类为一个 Model,一个 view,或是一个控制器。就这些,那么把网络代码放哪里?和一个 API 通信的代码应该放在哪儿?
你可能试着把它放在 Model 对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的 Model 生命周期更长,事情将变的复杂。显然也不应该把网络代码放在 view 里,因此只剩下控制器了。这同样是个坏主意,因为这加剧了厚重控制器的问题。那么应该放在那里呢?显然 MVC 的 3 大组件根本没有适合放这些代码的地方。
5)较差的可测试性
MVC 的另一个大问题是,它不鼓励开发人员编写单元测试。由于控制器混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。大多数人选择忽略这个任务,那就是不做任何测试。
上文提到了控制器可以管理视图的层次结构;控制器有一个 “view” 属性,并且可以通过 IBOutlet 访问视图的任何子视图。当有很多 outlet 时这样做不易于扩展,在某种意义上,最好不要使用子视图控制器(child view controller)来帮助管理子视图。在这里有多个模糊的标准,似乎没有人能完全达成一致。貌似无论如何,view 和对应的 controller 都紧紧的耦合在一起,总之,还是会把它们当成一个组件来对待。Apple 提供的这个组件一度以来在某种程度误导了大多初学者,初学者将所有的视图全部拖到 xib 中,连接大量的 IBoutLet 输出口属性,都是一些列问题。
2、MVC 的使用
Modal 模型的创建
Objective-C
// BookModel.h @interface BookModel : NSObject // 根据需要使用的数据创建数 Modal 数据模型属性变量 @property(nonatomic, copy)NSString *title; @property(nonatomic, copy)NSString *detail; @property(nonatomic, copy)NSString *icon; @property(nonatomic, copy)NSString *price; + (instancetype)bookModelWithDict:(NSDictionary *)dict; @end // BookModel.m @implementation BookModel + (instancetype)bookModelWithDict:(NSDictionary *)dict { BookModel *book = [[self alloc] init]; [book setValuesForKeysWithDictionary:dict]; return book; } @end
Swift
// BookModel.swift class BookModel: NSObject { // 根据需要使用的数据创建数 Modal 数据模型属性变量 var title:String? var detail:String? var icon:String? var price:String? }
View 视图的创建
Objective-C
// BookCell.h @class BookModel; @interface BookCell : UITableViewCell // 创建 Cell 视图包含的内容,Cell 使用 xib 创建 @property (weak, nonatomic) IBOutlet UIImageView *iconView; @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *detailLabel; @property (weak, nonatomic) IBOutlet UILabel *priceLabel; // 创建 Cell 视图赋值方法 @property (nonatomic, strong) BookModel *bookModel; @end // BookCell.m // 包含数据模型头文件 #import "BookModel.h" @implementation BookCell // 从 Model 数据模型中取出数据更新 View 的内容 - (void)setBookModel:(BookModel *)bookModel { _iconView.image = [UIImage imageNamed:bookModel.icon]; _titleLabel.text = bookModel.title; _detailLabel.text = bookModel.detail; _priceLabel.text = bookModel.price; } @end
Swift
// BookCell.swift class BookCell: UITableViewCell { // 创建 Cell 视图包含的内容,Cell 使用 xib 创建 @IBOutlet weak var iconView: UIImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var detailLabel: UILabel! @IBOutlet weak var priceLabel: UILabel! // 创建 Cell 视图赋值方法,从 Modal 数据模型中取出数据更新 View 的内容 func configWithModel(bookModel:BookModel) { iconView!.image = UIImage(named: bookModel.icon!) titleLabel!.text = bookModel.title detailLabel!.text = bookModel.detail priceLabel!.text = bookModel.price } }
Controller 控制器的创建
Objective-C
// ViewController.m // Modal 模型处理 // 声明数据源 @property (nonatomic, strong) NSArray *myDataArray; // 加载模型数据 - (NSArray *)myDataArray { if (_myDataArray == nil) { NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"]]; NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { // KVC BookModel *bookModel = [BookModel bookModelWithDict:obj]; // 使用 Modal 数据模型初始化数据源数组 [arrayM addObject:bookModel]; }]; _myDataArray = [arrayM copy]; } return _myDataArray; } // View 视图处理 UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20)]; myTableView.delegate = self; myTableView.dataSource = self; [myTableView registerNib:[UINib nibWithNibName:@"BookCell" bundle:nil] forCellReuseIdentifier:@"BookCell"]; [self.view addSubview:myTableView]; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [self.myDataArray count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 80; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ BookCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookCell" forIndexPath:indexPath]; // 从 Modal 数据模型中取出数据更新 View 的内容 cell.bookModel = self.myDataArray[indexPath.row]; return cell; }
Swift
// ViewController.swift // Modal 模型处理 var myDataArray:NSMutableArray! myDataArray = NSMutableArray() for bookInfoDic in NSArray(contentsOfFile: NSBundle.mainBundle().pathForResource("bookData", ofType: "plist")!)! { let bookModel = BookModel() // KVC bookModel.setValuesForKeysWithDictionary(bookInfoDic as! Dictionary) // 使用 Modal 数据模型初始化数据源数组 myDataArray.addObject(bookModel) } // View 视图处理 let myTableView:UITableView = UITableView(frame: CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20)) myTableView.delegate = self myTableView.dataSource = self myTableView.registerNib(UINib(nibName: "BookCell", bundle: nil), forCellReuseIdentifier: "BookCell") self.view.addSubview(myTableView) func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return myDataArray.count } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 80 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("BookCell", forIndexPath: indexPath) as! BookCell let bookModel:BookModel = myDataArray.objectAtIndex(indexPath.row) as! BookModel cell.configWithModel(bookModel) // 从 Modal 数据模型中取出数据更新 View 的内容 return cell }