MVVM设计模式教程 - tutorial with ReactiveCocoa

你可能在Twitter上看到过这样的笑话:
“iOS MVC架构,代表着杂乱无序的ViewController” - Colin Campbell
我敢确定大家开发中都碰到过,过大的、不可维护的view controller。


MVVM使用不同的架构模式,model-view-view model,简称MVVM。


这种模式,通过反射获得灵活性,是一个优秀的MVC替换模式,保证获得平滑的、轻量的view controller。


该MVVM教程从一个简单的flickr搜索应用开始,





在本教程开始前,我们回顾一下一些技术原理。


ReactiveCocoa扼要回顾


本教程是关于MVVM的重要文章,需要读者有一些ReactiveCocoa知识。
假如没有使用过ReactiveCocoa,强烈建议翻读我之前的ReactiveCocoa博客。


再次回顾一下ReactiveCocoa的要点,使读者对ReactiveCocoa有个快速概览。


ReactiveCocoa的核心是Signals,核心类RACSignal,Signals触发事件流(events stream), 包括类型,下一步,完成,以及错误等等。


使用这个简单的模式,ReactiveCocoa可以扮演委托模式(delegate pattern),行为目标模式(target-action patter), key-value模式,等等。


Signal API 代码干净,简单易读,而且在上层代码的很多操作中可以使用这些Signal。
你可以实现复杂的过滤器,发送模块,条件判断等高级设计模型。


在MVVM的上下文中,ReactiveCocoa扮演非常特俗的角色。它在ViewModel和View之间像提供了类似‘胶水’功能。


Model-View-ViewModel,即MVVM,是一个UI设计模式,是设计模式MV*家族中的一员,这些成员包括MVC,MVP(Model View Presenter),...。


上述的每个设计模式关注的是UI层和逻辑层(business logic)分离,为了使程序便于开发和测试。


Note:关于设计模式,我推荐Eli和Ash Furrow的文章。


为了更好理解MVVM,我们再看一下MVC。


MVC是第一个UI设计模式,它的原型来自70年代的SmallTalk语言。下面的图片展示了MVC模块的交互。




这个模式,Model维护着程序状态,UI控件、视图组成了View,而Controller处理了用户交互(Interactions),承担更新Model,View的事情。


一个大的问题关于MVC模式是太过模糊。原理看似简单,使用起来经常会使Model, View, Controller循环依赖。总之,越变越大,杂乱无章。


最近Martin Fowler介绍了MVC-Presentation Model, 就是采用了,并且微软也是倍加推崇的MVVM模式。


MVVM设计模式


MVVM模式核心模块是ViewModel,这个特殊的model表示了应用程序的UI状态。
它包含了每个UI控件,视图的状态(state)和属性。比如,它包含了TextField的文本(text), 或者某个Button的enable属性,同样包含了视图(view)可以操作的事件(action),点击(tap),手势(gesture)。


可以认为ViewModel是视图的模型(Model-of-the-view)。




MVVM的三个组件和MVC的类似,遵循以下精确的规则,
1. 视图(View)有一个ViewModel的引用,相反没有;
2. ViewModel有一个Model的引用,相反没有;
以上两点需要一起执行,
MVVM显而易见的优点是,
1. 视图层更加轻,UI的逻辑都在ViewModel中;
2. 测试更加方便,应用拿掉UI也可以运行;


由于ViewModel没有View的引用,那如何更新View?重点来了,


MVVM 和 数据绑定(Data Binding)
MVVM模式依靠数据绑定(Data Binding), 一个框架层面的特性自动连接对象属性到UI控件。





例如,微软的WPF框架,下面的配置把TextField的Text属性和ViewModel中的Username绑定
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>


TwoWay模块需要确保Username的改变能够传播到TextField的Text属性中,相反,用户输入时需要更新Username。


另外一个例子是Knockout(基于Web的MVVM框架),绑定的设置,
<input data-bind=”value: username”/>
一个HTML的元素和JavaScript模块中的元素绑定。


不幸的是iOS还缺少数据绑定框架,那么我们就用ReactiveCocoa来扮演这个角色。


从iOS开发的角度来看MVVM,ViewController和相关的UI-不管是nib,storyboard还是代码实现的,组成了View,


ReactiveCocoa绑定了View和ViewModel。


Note:推荐Martin Fowler的GUI Architectures article


Okey,我们开始创建一个ViewModel


Starter Project Sturecture


先下载初学者工程


它用CocoaPods来管理依赖。


这个工程已经包含了应用程序的ViewController和nib文件组成的视图模块。
打开RWTFlickrSearch.xcworkspace。


花点时间熟悉一下工程结构,





第一个ViewModel


添加一个新类,命名为RWTFlickrSearchViewModel,继承自NSObject,


@interface RWTFlickrSearchViewModel : NSObject
 
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
 
@end


searchText属性代表者页面上TextField的text属性,title代表了导航栏上的标题。


Note:为了更好理解应用结构,View和ViewModel取相同的名称,带不同的后缀,RWTFlickrSearch-ViewModel和RWTFlickrSearch-ViewConrtoller。


@implementation RWTFlickrSearchViewModel
 
- (instancetype)init {
  self = [super init];
  if (self) {
    [self initialize];
  }
  return self;
}
 
- (void)initialize {
  self.searchText = @"search text";
  self.title = @"Flickr Search";
}
 
@end


代码很简单。


下一步是ViewModel和View关联。记住视图模块(View)有一个ViewModel的引用。看起来Viewmodel的初始化和视图模块关联起来也比较合理。


#import "RWTFlickrSearchViewModel.h"


@interface RWTFlickrSearchViewController : UIViewController 
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
 
@end


在RWTFlickrSearchViewContrller.m中,添加一个私有的属性,


@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
  self = [super init];
  if (self ) {
    _viewModel = viewModel;
  }
  return self;
}


在View层保存一个ViewModel的弱引用。


在viewDidLoad函数中添加:


// 初始化绑定
[self bindViewModel];


- (void)bindViewModel {
  self.title = self.viewModel.title;
  self.searchTextField.text = self.viewModel.searchText;
}


刚才提到的是在ViewController中保存了一个弱引用,那么在哪里拥有实例呢?
这个例子中提到的是AppDelegate中,


在RWTAppDelegate.m中,


#import "RWTFlickrSearchViewModel.h"
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;


看看初始化函数,


- (UIViewController *)createInitialViewController {
  self.viewModel = [RWTFlickrSearchViewModel new];
  return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}





ViewModel文件已经添加了,下面该ReactiveCocoa登场了。


Detecting Vaild Search State


接下来我们需要ReactiveCocoa把ViewModel和View绑定在一起了。
在RWTFlickrSearchViewController.m中,更新bindViewModel,


- (void)bindViewModel {
  self.title = self.viewModel.title;
  RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}


添加一个rac_textSignal属性到UITextField类中,需要ReactiveCocoa来扩展。
当textfield更新后,就会触发包含当前text的一个事件(rac_textSignal)。


RAC宏就是viewModel中searchText和TextField rac_textSignal事件的一个绑定。


简而言之,确保searchText属性总是反映当前UI(TextField)的状态。


搜索按钮的enable状态和text输入是否有效关联在一起。在RWTFlickrSearchViewModel.m添加,


#import <ReactiveCocoa/ReactiveCocoa.h>
- (void)initialize {
  self.title = @"Flickr Search";
 
  RACSignal *validSearchSignal =
    [[RACObserve(self, searchText)
      map:^id(NSString *text) {
         return @(text.length > 3);
      }]
      distinctUntilChanged];
 
  [validSearchSignal subscribeNext:^(id x) {
    NSLog(@"search text is valid %@", x);
  }];
}


添加搜索命令


@property (strong, nonatomic) RACCommand *executeSearch;


- (void)initialize {
  self.title = @"Flickr Search";
 
  RACSignal *validSearchSignal =
    [[RACObserve(self, searchText)
      map:^id(NSString *text) {
         return @(text.length > 3);
      }]
      distinctUntilChanged];
 
  [validSearchSignal subscribeNext:^(id x) {
    NSLog(@"search text is valid %@", x);
  }];

self.executeSearch =
  [[RACCommand alloc] initWithEnabled:validSearchSignal
    signalBlock:^RACSignal *(id input) {
      return  [self executeSearchSignal];
    }];
}


- (RACSignal *)executeSearchSignal {
  return [[[[RACSignal empty]
           logAll]
           delay:2.0]
           logAll];
}


现在该搜索按钮绑定了,
在RWTFlickrSearchViewController.m中


- (void)bindViewModel {
  self.title = self.viewModel.title;
  
  // 属性绑定
  RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
  
  // 搜索事件绑定
  self.searchButton.rac_command = self.viewModel.executeSearch;
}


Model在哪里?


到这里,你可能有清晰认识到View(RWTFickrSearchViewController)和ViewModel(RWTFlickrSearchViewModel),那么Model在哪里?


这个列子中,用Search TextField中的text搜索,并且返回一组匹配的结果。
你可以在ViewModel中添加逻辑模块。


RWTFlickrSearch.m提供接口定义,


#import <ReactiveCocoa/ReactiveCocoa.h>
@import Foundation;
 
@protocol RWTFlickrSearch <NSObject>
 
- (RACSignal *)flickrSearchSignal:(NSString *)searchString;
 
@end


RWTFlickrSearchImpl提供了实现部分,


@import Foundation;
#import "RWTFlickrSearch.h"
 
@interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>
 
@end


implementation:
@implementation RWTFlickrSearchImpl
 
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
  return [[[[RACSignal empty]
            logAll]
            delay:2.0]
            logAll];
}
 
@end


RWTFlickrSearch详细的例子,方方面面都有涉及,请参考,

  example project


结论


  • MVVM设计模式获得流行;
  • MVVM模式可以把UI做的很轻很薄,增强了逻辑功能的可测性;
  • 精确的规则View => ViewModel => Model,ViewModel和View之间用绑定实现。
  • ViewModel不会持有View的引用,增强了View的可替换性。
  • ViewModel可以想像成model-of-the-view, 它暴露了反映View的属性,还有用户交互的方法。
  • Model层典型地用服务方式(Service)实现。
  • 一个很好的MVVM应用是可以没有UI层就可以运行。
  • ReactiveCocoa提供了很好的机制,使ViewModel和View绑定。

原文地址:
http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-2


ModernUI(http://mui.codeplex.com/)是一个开源的WPF界面库,利用该界面库,我们可以创建很酷的应用程序。下面是ModernUI官方示例,你可以从官方网站直接下载源码运行,如果是.NET 4.0的话,记得要声明“NET4”预编译变量,否则无法编译通过。 这个界面框架是基于ModernUI来实现的,在该文我将分享所有的源码,并详细描述如何基于ModernUI来构造一个非常通用的、插件化的WPF开发框架。下载源码的同志,希望点击一下推荐。 本文将按照以下四点来介绍: (1)ModernUI简介; (2)构建通用界面框架的思路; (3)基于ModernUI和OSGi.NET的插件化界面框架实现原理及源码分析; (4)其它更有趣的东西~~。 要编写这样的WPF界面,我们需要在一个Window上声明菜单和Tab页面,下图是定义菜单的声明。 此外,每一个Tab风格页面,你也需要手动的为菜单创建这样的界面元素。 直接用这样的方式来使用ModernUI,显然不太适合团队协作性的并行开发,因为在一个团队的协作中,不同的人需要完成不同的功能,实现不同页面,每个人都需要来更改主界面。 我非常希望模块化的开发方法,因为这可以尽可能的复用现有资产,使程序员可以聚焦在自己关注的业务逻辑上,不需要关心UI的使用。下面,我将来描述基于ModernUI实现的一个通用界面框架,这个界面框架允许程序员在自己的业务模块中配置需要显示的界面元素。 通用界面框架实现思路: 我希望能够实现这样的通用界面框架: (1)程序员可以直接实现需要展现业务逻辑的界面,不需要关注如何使用ModernUI; (2)程序员可以通过简单的配置就可以将自己实现的业务逻辑页面显示在主界面中; (3)这个界面框架可以完全复用。 当我看到ModernUI这个界面库时,我希望将应用程序做成模块化,每一个模块能够: (1)通过以下配置能够直接显示二级菜单。 (2)通过以下配置能够直接显示三级菜单。 这样做的好处是,开发插件的时候可以不需要关心界面框架插件;团队在协作开发应用的时候,可以独立开发并不需要修改主界面;团队成员的插件可以随时集成到这个主界面;当主界面无法满足我们的布局时或者用户需求无法满足时,可以直接替换主界面框架而不需要修改任何插件代码。 最终的效果如下,以下界面的几个菜单及点击菜单显示的内容由DemoPlugin插件、DemoPlugin2插件来提供。当插件框架加载更多插件时,界面上会出现更多的菜单;反之,当插件被卸载或者被停止时,则相应的菜单将消失掉。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值