ReactiveCocoa框架下的MVVM模式解读
记录一些MVVM文章中关于ReactiveCocoa的代码:
实例一:带有分页的文章列表,根据文章类别过滤出文章的列表,可以进入文章详细页面
1:YFBlogListViewModel 首先了解关于列表的ViewModel的代码内容:
#import <Foundation/Foundation.h> #import <ReactiveCocoa.h> @class YFCategoryArticleListModel; /** * 文章列表的视图模型. */ @interface YFBlogListViewModel : NSObject @property (copy, nonatomic) NSArray * blogListItemViewModels; //!< 文章.内部存储的应为文章列表单元格的视图模型.注意: 刷新操作,存储第一页数据;翻页操作,将存储所有的数据,并按页面排序. /** * 使用一个分类文章列表数据模型来快速初始化. * * @param model 文章列表模型. * * @return 实例对象. */ - (instancetype)initWithCategoryArtilceListModel: (YFCategoryArticleListModel *) model; /** * 获取首页的数据.常用于下拉刷新. * */ - (void)first; /** * 翻页,获取下一页的数据.常用于上拉加载更多. */ - (void)next; @end
#import "YFBlogListViewModel.h" #import <ReactiveCocoa.h> #import <AFNetworking.h> #import <RACAFNetworking.h> #import "YFCategoryArticleListModel.h" #import <MJExtension.h> #import "YFBlogListItemViewModel.h" #import "YFArticleModel.h" @interface YFBlogListViewModel () @property (strong, nonatomic) AFHTTPRequestOperationManager * httpClient; @property (strong, nonatomic) NSNumber * nextPageNumber; //!< 下次要请求第几页的数据. @property (copy, nonatomic) NSString * category; //!< 文章类别. @property (copy, nonatomic) NSString * requestPath; //!< 完整接口地址. @end @implementation YFBlogListViewModel - (instancetype)initWithCategoryArtilceListModel:(YFCategoryArticleListModel *)model { self = [super init]; if (nil != self) { // 设置 self.category 与 model.category 的关联. [RACObserve(model, category) subscribeNext:^(NSString * categoryName) { self.category = categoryName; }]; // 和类型无关的RAC 初始化操作,应该剥离出来. [self setup]; } return self; } /** * 和数据模型无关的初始化设置,放到独立的方法中. */ - (void)setup { // 初始化网络请求相关的信息. self.httpClient = [AFHTTPRequestOperationManager manager]; self.httpClient.requestSerializer = [AFJSONRequestSerializer serializer]; self.httpClient.responseSerializer = [AFJSONResponseSerializer serializer]; // 设置 self.nextPageNumber 与self.category的关联. [RACObserve(self, category) subscribeNext:^(id x) { // 只要分类变化,下次请求,都需要重置为请求第零页的数据. self.nextPageNumber = @0; }]; // 接口完整地址,肯定是受分类和页面的影响的.但是因为分类的变化最终会通过分页的变化来体现,所以此处仅需监测分页的变化情况即可. [RACObserve(self, nextPageNumber) subscribeNext:^(NSNumber * nextPageNumber) { NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=%@&model[page]=%@", self.category, nextPageNumber]; self.requestPath = path; }]; // 每次数据完整接口变化时,必然要同步更新 blogListItemViewModels 的值. [[RACObserve(self, requestPath) filter:^BOOL(id value) { return value; }] subscribeNext:^(NSString * path) { /** * 分两种情况: 如果是变为0,说明是重置数据;如果是大于0,说明是要加载更多数据;不处理向上翻页的情况. */ NSMutableArray * articls = [NSMutableArray arrayWithCapacity: 42]; if (YES != [self.nextPageNumber isEqualToNumber: @0]) { [articls addObjectsFromArray: self.blogListItemViewModels]; } [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) { // 使用MJExtension将JSON转换为对应的数据模型. NSArray * newArticles = [YFArticleModel objectArrayWithKeyValuesArray: JSONAndHeaders.first]; // RAC 风格的数组操作. RACSequence * newblogViewModels = [newArticles.rac_sequence map:^(YFArticleModel * model) { YFBlogListItemViewModel * vm = [[YFBlogListItemViewModel alloc] initWithArticleModel: model]; return vm; }]; [articls addObjectsFromArray: newblogViewModels.array]; self.blogListItemViewModels = articls; }]; }]; } - (void)first { self.nextPageNumber = @0; } - (void)next { self.nextPageNumber = [NSNumber numberWithInteger: [self.nextPageNumber integerValue] + 1]; } @end
2:YFCategoryArticleListModel模型的内容
#import <Foundation/Foundation.h> /** * 分类文章列表. */ @interface YFCategoryArticleListModel : NSObject @property (copy, nonatomic) NSString * category; //!< 分类 @property (strong, nonatomic) NSArray * articles; //!< 此分类下的文章列表. @end
3:ViewController的代码
#import <UIKit/UIKit.h> @class YFBlogListViewModel; @interface YFMVVMPostListViewController : UIViewController<UITableViewDelegate, UITableViewDataSource> @property (nonatomic, strong) UITableView * tableView; @property (strong, nonatomic) YFBlogListViewModel * viewModel; @end
#import "YFMVVMPostListViewController.h" #import "YFBlogListViewModel.h" #import <ReactiveCocoa.h> #import "YFCategoryArticleListModel.h" #import "YFBlogListViewModel.h" #import "YFBlogListItemViewModel.h" #import "YFArticleModel.h" #import "YFBlogDetailViewModel.h" #import <MJRefresh.h> #import "YFMVVMPostViewController.h" @interface YFMVVMPostListViewController () @end @implementation YFMVVMPostListViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [RACObserve(self.viewModel, blogListItemViewModels) subscribeNext:^(id x) { [self updateView]; }]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (UITableView *)tableView { if (nil == _tableView) { _tableView = [[UITableView alloc] init]; [self.view addSubview: _tableView]; [_tableView makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(UIEdgeInsetsMake(0, 0, 0, 0)); }]; _tableView.delegate = self; _tableView.dataSource = self; NSString * cellReuseIdentifier = NSStringFromClass([UITableViewCell class]); [_tableView registerClass: NSClassFromString(cellReuseIdentifier) forCellReuseIdentifier:cellReuseIdentifier]; _tableView.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ [self.viewModel first]; }]; _tableView.footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{ [self.viewModel next]; }]; } return _tableView; } /** * 更新视图. */ - (void) updateView { [self.tableView.header endRefreshing]; [self.tableView.footer endRefreshing]; [self.tableView reloadData]; } # pragma mark - tabelView代理方法. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger number = self.viewModel.blogListItemViewModels.count; return number; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString * cellReuseIdentifier = NSStringFromClass([UITableViewCell class]); UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: cellReuseIdentifier forIndexPath:indexPath]; YFBlogListItemViewModel * vm = self.viewModel.blogListItemViewModels[indexPath.row]; NSString * content = vm.intro; cell.textLabel.text = content; cell.selectionStyle = UITableViewCellSelectionStyleNone; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // 跳转到博客详情. YFBlogListItemViewModel * itemVM = self.viewModel.blogListItemViewModels[indexPath.row]; YFMVVMPostViewController * postVC = [[YFMVVMPostViewController alloc] init]; YFBlogDetailViewModel * detailVM = [[YFBlogDetailViewModel alloc] init]; detailVM.blogId = itemVM.blogId; postVC.viewModel = detailVM; [self.navigationController pushViewController: postVC animated: YES]; } @end
4:跳转到当前页面的内容
YFMVVMPostListViewController * mvvmPostVC = [[YFMVVMPostListViewController alloc] init]; YFCategoryArticleListModel * articleListModel = [[YFCategoryArticleListModel alloc] init]; articleListModel.category = @"ui"; YFBlogListViewModel * listVM = [[YFBlogListViewModel alloc] initWithCategoryArtilceListModel: articleListModel]; mvvmPostVC.viewModel = listVM; [self.navigationController pushViewController: mvvmPostVC animated: YES];
5:详细页面的ViewModel代码:
#import <Foundation/Foundation.h> @class YFArticleModel; /** * 文章详情的视图模型. */ @interface YFBlogDetailViewModel : NSObject @property (copy, nonatomic) NSString * content; // 要显示的内容. @property (copy, nonatomic) NSString * blogId; //!< 博客ID. - (instancetype)initWithModel: (YFArticleModel *) model; @end
#import "YFBlogDetailViewModel.h" #import <ReactiveCocoa.h> #import "YFArticleModel.h" #import <RACAFNetworking.h> #import <MJExtension.h> @interface YFBlogDetailViewModel () @property (strong, nonatomic) AFHTTPRequestOperationManager * httpClient; @property (copy, nonatomic) NSString * requestPath; //!< 完整接口地址. @end @implementation YFBlogDetailViewModel - (instancetype)init { self = [self initWithModel: nil]; return self; } - (instancetype)initWithModel:(YFArticleModel *)model { self = [super init]; if (nil != self) { // 设置self.blogId与model.id的相互关系. [RACObserve(model, id) subscribeNext:^(id x) { self.blogId = x; }]; [self setup]; } return self; } /** * 公共的与Model无关的初始化. */ - (void)setup { // 初始化网络请求相关的信息. self.httpClient = [AFHTTPRequestOperationManager manager]; self.httpClient.requestSerializer = [AFJSONRequestSerializer serializer]; self.httpClient.responseSerializer = [AFJSONResponseSerializer serializer]; // 接口完整地址,肯定是受id影响. [[RACObserve(self, blogId) filter:^BOOL(id value) { return value; }] subscribeNext:^(NSString * blogId) { NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=%@", blogId]; self.requestPath = path; }]; // 每次完整的数据接口变化时,必然要同步更新 self.content 的值. [[RACObserve(self, requestPath) filter:^BOOL(id value) { return value; }] subscribeNext:^(NSString * path) { [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) { // 使用MJExtension将JSON转换为对应的数据模型. YFArticleModel * model = [YFArticleModel objectWithKeyValues:JSONAndHeaders.first]; self.content = model.body; }]; }]; } @end
6:详细页面的ViewController
#import <UIKit/UIKit.h> @class YFBlogDetailViewModel; @interface YFMVVMPostViewController : UIViewController @property (strong, nonatomic) YFBlogDetailViewModel * viewModel; @end
#import "YFMVVMPostViewController.h" #import "YFBlogDetailViewModel.h" #import <ReactiveCocoa.h> @interface YFMVVMPostViewController () @property (strong, nonatomic) UIWebView * webView; @end @implementation YFMVVMPostViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [RACObserve(self.viewModel, content) subscribeNext:^(id x) { [self updateView]; }]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (UIWebView *)webView { if (nil == _webView) { _webView = [[UIWebView alloc] init]; [self.view addSubview: _webView]; [_webView makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(UIEdgeInsetsMake(0, 0, 0, 0)); }]; } return _webView; } /** * 更新视图. */ - (void) updateView { [self.webView loadHTMLString: self.viewModel.content baseURL:nil]; } @end
实例二:用户列表实例
1:用户列表的ViewModel代码UsersViewModel
#import <ReactiveViewModel/ReactiveViewModel.h> @class RACCommand; #pragma mark - @interface UsersViewModel : RVMViewModel /// Array of UserViewModel objects filled by userViewModelsCommand. @property (nonatomic, readonly) NSArray *userViewModels; /// Input: nil @property (nonatomic, readonly) RACCommand *userViewModelsCommand; /// Input: nil @property (nonatomic, readonly) RACCommand *clearImageCacheCommand; @property (nonatomic, readonly, getter=isLoading) BOOL loading; @end
#import "UsersViewModel.h" #import "UserViewModel.h" #import "UserController.h" #import "User.h" #import "ImageController.h" #import <ReactiveCocoa/ReactiveCocoa.h> #import <ReactiveCocoa/RACEXTScope.h> #pragma mark - @implementation UsersViewModel - (instancetype)init { self = [super init]; if (self) { UserController *userController = [[UserController alloc] init]; ImageController *imageController = [ImageController sharedController]; _userViewModelsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id _) { return [[[userController fetchRandomUsers:100] subscribeOn:[RACScheduler scheduler]] map:^NSArray *(NSArray *users) { return [[[users rac_sequence] map:^UserViewModel *(User *user) { return [[UserViewModel alloc] initWithUser:user imageController:imageController]; }] array]; }]; }]; RAC(self, userViewModels) = [[[_userViewModelsCommand executionSignals] switchToLatest] deliverOn:[RACScheduler mainThreadScheduler]]; RAC(self, loading) = [_userViewModelsCommand executing]; _clearImageCacheCommand = [[RACCommand alloc] initWithEnabled:[RACObserve(self, loading) not] signalBlock:^RACSignal *(id _) { return [imageController purgeLocalCaches]; }]; } return self; } @end
2:另外封装UserController的代码:
@class RACSignal; #pragma mark - @interface UserController : NSObject /// Sends an array of fabricated User objects then completes. - (RACSignal *)fetchRandomUsers:(NSUInteger)numberOfUsers; @end
#import "UserController.h" #import "User.h" #import <ReactiveCocoa/ReactiveCocoa.h> #import <LoremIpsum/LoremIpsum.h> #pragma mark - @implementation UserController - (RACSignal *)fetchRandomUsers:(NSUInteger)numberOfUsers { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSMutableArray *usersArray = [NSMutableArray array]; for (int i = 0; i < numberOfUsers; i++) { NSString *name = [LoremIpsum name]; NSURL *avatarURL = [[LoremIpsum URLForPlaceholderImageFromService:LIPlaceholderImageServiceHhhhold withSize:CGSizeMake(96, 96)] URLByAppendingPathComponent:[NSString stringWithFormat:@"jpg?test=%i", i]]; User *user = [[User alloc] initWithName:name avatarURL:avatarURL]; [usersArray addObject:user]; } [subscriber sendNext:[usersArray copy]]; [subscriber sendCompleted]; return nil; }]; } @end
3:Model的代码:
#pragma mark - @interface User : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) NSURL *avatarURL; - (instancetype)initWithName:(NSString *)name avatarURL:(NSURL *)avatarURL; @end
#import "User.h" #pragma mark - @implementation User - (instancetype)initWithName:(NSString *)name avatarURL:(NSURL *)avatarURL { self = [super init]; if (self != nil) { _name = name; _avatarURL = avatarURL; } return self; } @end
4:ViewController的代码
@class UsersViewModel; @interface UsersViewController : UITableViewController - (instancetype)initWithViewModel:(UsersViewModel *)viewModel; @end
#import "UsersViewController.h" #import "UsersViewModel.h" #import "UserViewModel.h" #import "UserCell.h" #import <ReactiveCocoa/ReactiveCocoa.h> #import <ReactiveCocoa/RACEXTScope.h> #pragma mark - @interface UsersViewController () @property (nonatomic, readonly) UsersViewModel *viewModel; @end @implementation UsersViewController - (instancetype)initWithViewModel:(UsersViewModel *)viewModel { self = [super init]; if (self != nil) { _viewModel = viewModel; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.tableView.rowHeight = 48; [self.tableView registerClass:[UserCell class] forCellReuseIdentifier:NSStringFromClass([UserCell class])]; @weakify(self); self.title = NSLocalizedString(@"Random Users", nil); UIBarButtonItem *clearImageCacheBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Clear Cache", nil) style:UIBarButtonItemStylePlain target:nil action:nil]; clearImageCacheBarButtonItem.rac_command = self.viewModel.clearImageCacheCommand; self.navigationItem.rightBarButtonItem = clearImageCacheBarButtonItem; self.refreshControl = [[UIRefreshControl alloc] init]; [[[self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged] mapReplace:self.viewModel.userViewModelsCommand] subscribeNext:^(RACCommand *userViewModelsCommand) { [userViewModelsCommand execute:nil]; }]; [RACObserve(self.viewModel, loading) subscribeNext:^(NSNumber *loading) { @strongify(self); if ([loading boolValue]) { [self.refreshControl beginRefreshing]; [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; } else { [self.refreshControl endRefreshing]; [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; } }]; [[RACObserve(self.viewModel, userViewModels) ignore:nil] subscribeNext:^(id _) { @strongify(self); [self.tableView reloadData]; }]; [self.viewModel.userViewModelsCommand execute:nil]; } #pragma mark UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.viewModel.userViewModels count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UserViewModel *viewModel = self.viewModel.userViewModels[indexPath.row]; UserCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([UserCell class]) forIndexPath:indexPath]; cell.viewModel = viewModel; cell.viewModel.active = YES; return cell; } # pragma mark UITableViewDelegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UserViewModel *userViewModel = self.viewModel.userViewModels[indexPath.row]; NSLog(@"Selected: %@", userViewModel); [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { UserCell *userCell = (UserCell *)cell; userCell.viewModel.active = NO; } @end
6:UserCell代码:
@class UserViewModel; #pragma mark - @interface UserCell : UITableViewCell @property (nonatomic) UserViewModel *viewModel; @end
#import "UserCell.h" #import "ImageView.h" #import "UserViewModel.h" #import "ImageViewModel.h" #import <ReactiveCocoa/ReactiveCocoa.h> #pragma mark - @interface UserCell () @property (nonatomic, readonly) ImageView *avatarImageView; @end @implementation UserCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self != nil) { _avatarImageView = [[ImageView alloc] init]; [self.contentView addSubview:_avatarImageView]; } return self; } - (void)setViewModel:(UserViewModel *)viewModel { if (_viewModel == viewModel) return; _viewModel = viewModel; self.avatarImageView.viewModel = _viewModel.imageViewModel; self.textLabel.text = _viewModel.name; } - (void)layoutSubviews { [super layoutSubviews]; self.avatarImageView.frame = CGRectMake(0, 0, 48, 48); self.textLabel.frame = CGRectMake(58, 0, 260, 48); } @end
7:调用主控制器跳转
UsersViewModel *usersViewModel = [[UsersViewModel alloc] init];
UsersViewController *usersViewController = [[UsersViewController alloc] initWithViewModel:usersViewModel];
另:reactivecocoa afnetworking地交互可以查下面这个实例,地址:https://github.com/octokit/octokit.objc
小项目框架设计(ReactiveCocoa+MVVM+AFNetworking+FMDB) 地址:http://www.tuicool.com/articles/Q3uuQvA