MVVM与ReactiveCocoa的运用(Part2)

  • 绑定,绑定,绑定(重要的实情说三遍)

RACCommand能实时地更新search按钮的状态,但是时候来处理activity indicator的可见状态了.
RACCommand拥有一个执行的属性,它是用来表示命令开始和结束执行时反应真假事件的信号量.你可以通过这个信号量来反映程序中当前命令执行的状态.
在RWTFlickrSearchViewController.m的bindViewModel的尾部添加:

RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) = self.viewModel.executeSearch.executing;

以上代码用来将UIApplication中的networkActivityIndicatorVisible属性和命令执行信号绑定.用来保证当命令执行时,小的网络激活状态标志在status bar里显示.
下一步,添加:

RAC(self.loadingIndicator, hidden) = [self.viewModel.executeSearch.executing not];

当指令执行后,加载标志将会被隐藏;这和你刚刚绑定的属性相反.
ReactiveCocoa已经为我们提供了不执行的相反的信号.最后,添加如下代码:

[self.viewModel.executeSearch.executionSignals
  subscribeNext:^(id x) {
    [self.searchTextField resignFirstResponder];
  }];

上面的代码用来保证当命令执行时键盘会隐藏.executionSignals属性用来在命令执行时实时地发出信号.
这是个signals中的一个signal属性(前面教程里有介绍).当一个新的命令执行的时候就会被创建和执行,隐藏键盘.
运行程序,来验证以上代码的执行.

  • Model呢?

到现在为止,你已经定义了一个View(RWTFlickrSearchViewController)和ViewModel(RWTFlickrSearchViewModel),但是,怎么木有Model呢?
答案很简单:就是还没有啊!
当前app用户点击搜索按钮后就会执行命令,但却没有实现什么.
我们需要实现的是利用当前输入的搜索文本通过ViewModel在Flickr进行搜索,继而返回相匹配的图片列表.
你可以将此逻辑直接放在ViewModel里,但相信我,你会后悔的!如果是个view controller,我到时强烈你这么做.
View Model拥有UI状态的属性,而且还能够执行命令(经常为UI上的动作方法).通过用户的交互来管理改变UI状态.
然而,并不表示这些交互实际的业务逻辑应该在View Model里面.而这应该是Model的工作.
下一步,将会给应用增加Model层.
在Model group里添加一个名为RWTFlickrSearch的新协议并提供了如下的方法:

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

这个协议定义了Model层的初始方法,用来将负责搜索Flickr的任务从ViewModel里移出.
接下来,在同一group里创建一个名为RWTFlickrSearchImpl的NSObject的子类.并使其遵从刚才的协议:

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

在RWTFlickrSearchImpl.m里添加以下代码:

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

是不是觉得似曾相识?如果是的,那是因为这个同一'虚拟'的实现曾经位于ViewModel里.
下一步是要在ViewModel里使用Model层.在ViewModel group里添加一个名为RWTViewModelServices的新协议:

@import Foundation;
#import "RWTFlickrSearch.h"
 
@protocol RWTViewModelServices <NSObject>
 
- (id<RWTFlickrSearch>) getFlickrSearchService;
 
@end

这个协议定义了ViewModel获得对RWTFlickrSearch协议引用的方法.
在RWTFlickrSearchViewModel.h导入这个新协议:

#import "RWTViewModelServices.h"

更新initializer来将它作为参数:

- (instancetype) initWithServices:(id<RWTViewModelServices>)services;

在RWTFlickrSearchViewModel.m里添加一个类扩展和一个私有属性来保存对view model services的引用:

@interface RWTFlickrSearchViewModel ()
 
@property (nonatomic, weak) id<RWTViewModelServices> services;
 
@end

在同一文件里更新initializer:

- (instancetype) initWithServices:(id<RWTViewModelServices>)services {
  self = [super init];
  if (self) {
    _services = services;
    [self initialize];
  }
  return self;
}

以上用来保存对services的引用.
最后,更新executeSearchSignal方法:

- (RACSignal *)executeSearchSignal {
  return [[self.services getFlickrSearchService]
           flickrSearchSignal:self.searchText];
}

上面的方法代理了model来实现搜索.
最后一步是将Model和ViewModel相连.
在RWTFlickrSearch项目的'root' group里添加一个名为RWTViewModelServiceImpl的NSObject的子类.在RWTViewModelServicesImpl.h里添加遵从RWTViewModelServices协议:

@import Foundation;
#import "RWTViewModelServices.h"
 
@interface RWTViewModelServicesImpl : NSObject <RWTViewModelServices>
 
@end

在RWTViewModelServicesImpl.m实现:

#import "RWTViewModelServicesImpl.h"
#import "RWTFlickrSearchImpl.h"
 
@interface RWTViewModelServicesImpl ()
 
@property (strong, nonatomic) RWTFlickrSearchImpl *searchService;
 
@end
 
@implementation RWTViewModelServicesImpl
 
- (instancetype)init {
  if (self = [super init]) {
    _searchService = [RWTFlickrSearchImpl new];
  }
  return self;
}
 
- (id<RWTFlickrSearch>)getFlickrSearchService {
  return self.searchService;
}
 
@end

这个类创建了一个RWTFlickrSearchImpl的实例,Model层服务Flickr搜索,将其提供给ViewModel upon 请求.
最后,在RWTAppDelegate.m里导入:

#import "RWTViewModelServicesImpl.h"

添加一个私有属性:

@property (strong, nonatomic) RWTViewModelServicesImpl *viewModelServices;

更新createInitialViewController方法:

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

运行程序,确保程序和之前运行的结果相似.
这并不是最令人兴奋的变化,但花点时间来看看新代码的"形状".
Model层展示了'service'的ViewModel consumes.协议定义了这个服务接口,提供松耦合.
你可以用这个假设的服务实现用于单元测试.应用现在已经有了正确地Model-View-ViewModel结构.来小结下:

  1. Model提供应用业务逻辑实现的服务.在本应用中,它提供了Flickr搜索的服务.
  2. ViewModel层提供了应用的视图状态.它提供了用户交互以及来自视图变化后Model层的事件.
  3. View层非常瘦,提供了ViewModel状态变化后在视图层的展示.
  • Flickr搜索

本章节,你将编写一个真实的Flickr搜索实现,事情开始变得令人兴奋了呢;]
第一步是创建一个搜索结果的model.
在Model group里添加一个名为RWTFlickrPhoto的NSObject的子类,在接口里添加三个属性:

@interface RWTFlickrPhoto : NSObject
 
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSString *identifier;
 
@end

这个Model对象相当于Flickr搜索API所返回的单张图片.
在RWTFlickrPhoto.m添加如下方法:

- (NSString *)description {
  return self.title;
}

这个方法可以在实现UI变化之前通过打印搜索的结果来测试搜索的实现.
接下来,添加另一个名为RWTFlickrSearchResults的NSObject子类的模型.在接口里添加如下属性:

@import Foundation;
 
@interface RWTFlickrSearchResults : NSObject
 
@property (strong, nonatomic) NSString *searchString;
@property (strong, nonatomic) NSArray *photos;
@property (nonatomic) NSUInteger totalResults;
 
@end

用来保存Flickr搜索返回的图片集合.
在RWTFlickrSearchResults.m的description方法里添加:

- (NSString *)description {
  return [NSString stringWithFormat:@"searchString=%@, totalresults=%lU, photos=%@",
          self.searchString, self.totalResults, self.photos];
}

现在开始编写Flickr搜索的代码喽!
在RWTFlickrSearchImpl.m添加:

#import "RWTFlickrSearchResults.h"
#import "RWTFlickrPhoto.h"
#import <objectiveflickr/ObjectiveFlickr.h>
#import <LinqToObjectiveC/NSArray+LinqExtensions.h>

导入了刚才你创建的Model,添加了一对CocoaPods添加的扩展依赖:

  • ObjectiveFlickr:这是个用Objective-C API 实现的Flickr API.用来处理授权和解析API返回的结果.使用此库比直接调用Flickr API要简单.
  • LingToObjectiveC:提供了流畅和丰富的查询接口,过滤和转换数组和词典.

仍然在RWTFlickrSearchImpl.m里添加类的扩展:

@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate>
 
@property (strong, nonatomic) NSMutableSet *requests;
@property (strong, nonatomic) OFFlickrAPIContext *flickrContext;
 
@end

使其遵从ObejectiveFlickr库的OFFlickrAPIRequestDelegate协议并添加了一对私有属性.你将很快看到他们的用法.
紧接着添加如下initializer:

- (instancetype)init {
  self = [super init];
  if (self) {
    NSString *OFSampleAppAPIKey = @"YOUR_API_KEY_GOES_HERE";
    NSString *OFSampleAppAPISharedSecret = @"YOUR_SECRET_GOES_HERE";
 
    _flickrContext = [[OFFlickrAPIContext alloc] initWithAPIKey:OFSampleAppAPIKey
                                                  sharedSecret:OFSampleAppAPISharedSecret];
    _requests = [NSMutableSet new];
  }
  return  self;
}

以上代码创建了一个Flickr 'context'来存储API请求所需的ObjectiveFlickr数据.
你可以在Flickr App Garden来获取Key.
ObjectiveFlickr API非常常规.你创建了一个API请求后返回结果成功或失败是通过定义的OFFlickrAPIRequestDelegate方法来处理的.
当前API是通过你的Model层服务类即RWTFlickrSearch协议,有一个方法来通过文本搜索字符串来进行图片搜索的.
然而,待会你将添加一些另外的方法.
因此,你将要去从直接用通用的方法到使用这种基于代理的API信号.
仍然在RWTFlickrSearchImpl.m文件里添加:

- (RACSignal *)signalFromAPIMethod:(NSString *)method
                         arguments:(NSDictionary *)args
                         transform:(id (^)(NSDictionary *response))block {
 
  // 1. Create a signal for this request
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
 
    // 2. Create a Flick request object
    OFFlickrAPIRequest *flickrRequest =
      [[OFFlickrAPIRequest alloc] initWithAPIContext:self.flickrContext];
    flickrRequest.delegate = self;
    [self.requests addObject:flickrRequest];
 
    // 3. Create a signal from the delegate method
    RACSignal *successSignal =
      [self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:)
                     fromProtocol:@protocol(OFFlickrAPIRequestDelegate)];
 
    // 4. Handle the response
    [[[successSignal
      map:^id(RACTuple *tuple) {
        return tuple.second;
      }]
      map:block]
      subscribeNext:^(id x) {
        [subscriber sendNext:x];
        [subscriber sendCompleted];
      }];
 
    // 5. Make the request
    [flickrRequest callAPIMethodWithGET:method
                              arguments:args];
 
    // 6. When we are done, remove the reference to this request
    return [RACDisposable disposableWithBlock:^{
      [self.requests removeObject:flickrRequest];
    }];
  }];
}

这个方法提供了一个API请求,详见方法名和传递的参数,继而通过block来传递结果.你将很快来了解它是如何工作的.
解析此方法,这里有许多内容.下面是每步的释义:

  1. createSignal方法创建了一个新信号.传递信号的block方法可以让你处理发送下一步、错误或者事件的完成信号.
  2. ObjectivewFlickr请求被创建,这个请求的引用被存储在请求set里.如果没有这段代码,OFFlickrAPIRequest将不被保留.
  3. rac_signalForSelector:fromProtocol方法从表示Flickr API 请求完成后的的代理方法里创建一个信号.
  4. 该信号被绑定,结果变换和被发送的结果作为信号被创建.
  5. ObjectiveFlickr API 请求被使用.
  6. 当信号被释放后,block确保Flickr请求的引用被移除,避免内存漏洞.

现在我们来看下步骤4的更多细节:

[[[successSignal
  // 1. Extract the second argument
  map:^id(RACTuple *tuple) {
    return tuple.second;
  }]
  // 2. transform the results
  map:block]
  subscribeNext:^(id x) {
    // 3. send the results to the subscribers
    [subscriber sendNext:x];
    [subscriber sendCompleted];
  }];

rac_signalForSelector:fromProtocol:方法创建了successSignal,而且它还从代理方法里创建信号.
当代理方法被调用时,下一个事件包含方法参数的RACTuple被发出.接着执行以下步骤:

  1. 一个map操作提取从flickrAPIRequest:didCompleteWithResponse:的第二个参数,代理方法中的词典结果.
  2. block传递给此方法一个结果参数.你将很快看到如何将词典转换为模型对象.
  3. 最后,传递的结果被发送给下一个事件,这个信号完成.

最后来实现Flickr搜搜结果方法:

- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
  return [self signalFromAPIMethod:@"flickr.photos.search"
                         arguments:@{@"text": searchString,
                                     @"sort": @"interestingness-desc"}
                         transform:^id(NSDictionary *response) {
 
    RWTFlickrSearchResults *results = [RWTFlickrSearchResults new];
    results.searchString = searchString;
    results.totalResults = [[response valueForKeyPath:@"photos.total"] integerValue];
 
    NSArray *photos = [response valueForKeyPath:@"photos.photo"];
    results.photos = [photos linq_select:^id(NSDictionary *jsonPhoto) {
      RWTFlickrPhoto *photo = [RWTFlickrPhoto new];
      photo.title = [jsonPhoto objectForKey:@"title"];
      photo.identifier = [jsonPhoto objectForKey:@"id"];
      photo.url = [self.flickrContext photoSourceURLFromDictionary:jsonPhoto
                                                              size:OFFlickrSmallSize];
      return photo;
    }];
 
    return results;
  }];
}

以上方法使用之前你添加的signalFromAPIMethod:arguments:transform:方法.flickr.photos.search API方法搜索图片,将以词典为格式.
传递到参数中的block简单地转换词典结果为模型对象,使ViewModel更容易使用.
代码使用linq_select方法通过LingToObjectiveC添加数组.提供了API的数组传送.
最后在RWTFlickrSearchViewModel.m里更新搜索信号日志结果:

- (RACSignal *)executeSearchSignal {
  return [[[self.services getFlickrSearchService]
           flickrSearchSignal:self.searchText]
           logAll];
}

运行,输入一个搜索字符后在console里查看信号的结果日志:

2014-06-03 [...] <RACDynamicSignal: 0x8c368a0> name: +createSignal: next: searchString=wibble, totalresults=1973, photos=(
    "Wibble, wobble, wibble, wobble",
    "unoa-army",
    "Day 277: Cheers to the freakin' weekend!",
    [...]
    "Angry sky",
    Nemesis
)

Girl学iOS100天 第18天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容