加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

ReactiveCocoa学习

发布时间:2020-12-15 05:18:07 所属栏目:百科 来源:网络整理
导读:ReactiveCocoa - iOS开发的新框架 RAC具有函数式编程和响应式编程的特性。它主要吸取了.Net的 Reactive Extensions的设计和实现。 ReactiveCocoa试图解决什么问题 传统iOS开发过程中,状态以及状态之间依赖过多的问题 在开发iOS应用时,一个界面元素的状态很

ReactiveCocoa - iOS开发的新框架

RAC具有函数式编程和响应式编程的特性。它主要吸取了.Net的 Reactive Extensions的设计和实现。

ReactiveCocoa试图解决什么问题

传统iOS开发过程中,状态以及状态之间依赖过多的问题

  • 在开发iOS应用时,一个界面元素的状态很可能受多个其它界面元素或后台状态的影响。
  • RAC通过引入信号(Signal)的概念,来代替传统iOS开发中对于控件状态变化检查的代理(delegate)模式或target-action模式。
  • 因为RAC的信号是可以组合(combine)的,所以可以轻松地构造出另一个新的信号出来。
  • 引入RAC之后,以前散落在action-target或KVO的回调函数中的判断逻辑被统一到了一起。
  • 除了组合(combine)之外,RAC的信号还支持链式(chaining)和过滤(filter),以方便将信号进行进一步处理。

传统MVC架构的问题:Controller比较复杂,可测试性差

  • 对于传统的Model-View-Controller的框架,Controller很容易变得比较庞大和复杂。
  • Controller常常与对应的View和Model的耦合度非常高,这同时也造成对其做单元测试非常不容易。
  • RAC的信号机制很容易将某一个Model变量的变化与界面关联,所以非常容易应用Model-View-ViewModel 框架。
  • 通过引入ViewModel层,然后用RAC将ViewModel与View关联,View层的变化可以直接响应ViewModel层的变化,这使得Controller变得更加简单,由于View不再与Model绑定,也增加了View的可重用性。
  • 因为引入了ViewModel层,所以单元测试可以在ViewModel层进行,iOS工程的可测试性也大大增强了。

提供统一的消息传递机制

  • iOS开发中有着各种消息传递机制,包括KVO、Notification、delegation、block以及target-action方式。各种消息传递机制使得开发者在做具体选择时感到困惑。
  • RAC将传统的UI控件事件进行了封装,使得以上各种消息传递机制都可以用RAC来完成。

ReactiveCocoa需要注意的

RAC在应用中大量使用了block,由于Objective-C语言的内存管理是基于引用计数 的,为了避免循环引用问题,在block中如果要引用self,需要使用@weakify(self)和@strongify(self)来避免强引用。

在使用时应该注意block的嵌套层数,不恰当的滥用多层嵌套block可能给程序的可维护性带来灾难。

ReactiveCocoa入门教程——第一部分

在编写iOS代码时,我们的大部分代码都是在响应一些事件:按钮点击、接收网络消息、属性变化等等。但是这些事件在代码中的表现形式却不一样:如target-action、代理方法、KVO、回调或其它。ReactiveCocoa的目的就是定义一个统一的事件处理接口,这样它们可以非常简单地进行链接、过滤和组合。

ReactiveCocoa思想和原理

ReactiveCocoa结合了一些编程模式:

  • 函数式编程:利用高阶函数,即将函数作为其它函数的参数。
  • 响应式编程:关注于数据流及变化的传播。

ReactiveCocoa有很多操作来控制事件流。假设你只关心超过3个字符长度的用户名,那么你可以使用filter操作来实现这个目的。把之前加在viewDidLoad中的代码更新成下面的:


[[self.usernameTextField.rac_textSignal
    filter:^BOOL(id value) {
        NSString *text = value;
        return text.length > 3;
    }]
    subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];

上面代码等价于:


RACSignal *usernameSourceSignal = self.usernameTextField.rac_textSignal; RACSignal *filteredUsername = [usernameSourceSignal filter:3; }]; [filteredUsername subscribeNext:id x) { NSLog(x); }];

subscribeNext定义在RACSignal.h文件中;filter定义在RACStream.h文件中。

ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。

Objective-C 2.0中的Category语法 ,Category提供了一种比继承(inheritance)更为简洁的方法来对class进行扩展,我们可以为任何已经存在的class添加方法。

ReactiveCocoa的核心就是信号,而它不过就是事件流。

RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。

Objective-C中的block

ReactiveCocoa大量使用block,关于在Objective-C中的声明有以下几种类型:


// As a local variable: returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...}; // As a property: @property (nonatomic, copy) returnType (^blockName)(parameterTypes); // As a method parameter: - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName; // As an argument to a method call: [someObject someMethodThatTakesABlock:^returnType (parameters) {...}]; // As a typedef: typedef returnType (^TypeName)(parameterTypes); TypeName blockName ^returnType(parameters) {...};

ReactiveCocoa的使用

用RAC判断用户名密码是否符合要求,并体现在界面上的一段代码:


#import "RWDummySignInService.h" #import "RWViewController.h" #import <ReactiveCocoa/ReactiveCocoa.h> @interface RWViewController () weak,0); font-weight:bold">nonatomic) IBOutlet UITextField *usernameTextField; *passwordTextField; UIButton *signInButton; UILabel *signInFailureText; strong,0); font-weight:bold">nonatomic) RWDummySignInService *signInService; @end @implementation RWViewController - (viewDidLoad { [super viewDidLoad]; self.signInService = [RWDummySignInService new]; // initially hide the failure message self.signInFailureText.hidden = YES; //////////////////////////////////////////////////// // Use RAC /**************************************************** * 首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。 ***************************************************/ RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignal map:id(*text) { return @([self isValidUsername:text]); }]; RACSignal *validPasswordSignal self.passwordTextField.rac_textSignal map:self isValidPassword:text]); }]; * 根据条件设置文本框背景是否为黄色,优雅的做法是下面代码中使用RAC宏 [[validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }] subscribeNext:^(UIColor *color) { self.passwordTextField.backgroundColor = color; }]; * RAC宏允许直接把信号的输出应用到对象的属性上。 * RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。 * 每次信号产生一个next事件,传递过来的值都会应用到该属性上。 ***************************************************/ RAC(self.passwordTextField, backgroundColor) = [validPasswordSignal map:NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }]; RAC(self.usernameTextField,0); font-weight:bold">= [validUsernameSignal map:UIColor yellowColor]; }]; * RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。 * ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。 * 实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。 *signUpActiveSignal = [RACSignal combineLatest:@[ validUsernameSignal, validPasswordSignal ] reduce:*usernameValid, *passwordValid) { return @([usernameValid boolValue] && [passwordValid boolValue]); }]; [signUpActiveSignal subscribeNext:^(*signupActive) { self.signInButton.enabled = [signupActive boolValue]; }]; * flattenMap: 解决信号中的信号(Signal of Signals)问题。 * doNext: 添加附加操作(Adding side-effects),而且doNext: block并没有返回值。 因为它是附加操作,并不改变事件本身。 ***************************************************/ [[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:id x) { NO; YES; }] flattenMap:id(id x) { return [self signInSignal]; }] subscribeNext:*signedIn) { YES; BOOL success = [signedIn boolValue]; = success; if (success) { // 转入下一个页面 [self performSegueWithIdentifier:@"signInSuccess" sender:self]; } }]; //////////////////////////////////////////////////// } - (BOOL)isValidUsername:(*)username { return username.length 3; } - (isValidPassword:(password { return password.length 3; } * 建了一个信号,使用用户名和密码登录。 * 把已有的异步API用信号的方式来表示相当简单。 ***************************************************/ - (RACSignal *)signInSignal { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [self.signInService signInWithUsername:self.usernameTextField.text password:self.passwordTextField.text complete:BOOL success) { [subscriber sendNext:@(success)]; [subscriber sendCompleted]; }]; return nil; }]; } @end
上面代码的逻辑图如下所示:

上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。

  • 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。
  • 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。

这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。

ReactiveCocoa入门教程——第二部分

如何取消订阅一个signal

ReactiveCocoa设计的一个目标就是支持匿名生成管道编程风格。为了支持这种模型,ReactiveCocoa自己持有全局的所有信号。如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。

如何取消订阅一个signal?在一个completed或者error事件之后,订阅会自动移除。还可以通过RACDisposable 手动移除订阅。这个方法并不常用到,但是还是有必要知道可以这样做。


*backgroundColorSignal = [self.searchText.rac_textSignal map:*text) { self isValidSearchText:text] UIColor whiteColor] UIColor yellowColor]; }]; RACDisposable *subscription = [backgroundColorSignal subscribeNext:UIColor *color) { self.searchText.backgroundColor = color; }]; // at some point in the future ... [subscription dispose];??
如果你创建了一个管道,但是没有订阅(subscribeNext:)它,这个管道就不会执行,包括任何如doNext: block的附加操作。

避免循环引用

subscribeNext:block中使用了self来获取text field的引用。block会捕获并持有其作用域内的值。因此,如果self和这个信号之间存在一个强引用的话,就会造成循环引用。循环引用是否会造成问题,取决于self对象的生命周期。如果self的生命周期是整个应用运行时,比如说本例,那也就无伤大雅。但是在更复杂一些的应用中,就不是这么回事了。

为了避免潜在的循环引用,Apple的文档Working With Blocks中建议获取一个self的弱引用。比如下面这样使用的:


__weak typeof(self) weakSelf self; // Capture the weak reference [[self.searchText.rac_textSignal map:*text) { UIColor yellowColor]; }] subscribeNext:*color) { __strong self) strongSelf = weakSelf strongSelf.searchText.backgroundColor = color; }]; ReactiveCocoa框架中的等价写法:


@weakify(self); [[self.searchText.rac_textSignal map:*text) { UIColor yellowColor]; }] subscribeNext:*color) { @strongify(self); = color; }];
ReactiveCocoa使用

下面代码实现搜索Twitter上的内容,并根据输入实时更新搜索结果。涉及到:线程、节流、error 和 completed的使用。


//
// RWSearchFormViewController.m // TwitterInstant // Created by Colin Eberhardt on 02/12/2013. // Copyright (c) 2013 Colin Eberhardt. All rights reserved. // #import "RWSearchFormViewController.h" #import "RWSearchResultsViewController.h" #import "RWTweet.h" #import <Accounts/Accounts.h> #import <LinqToObjectiveC/NSArray+LinqExtensions.h> // NSArray+LinqExtensions.h头文件是LinqToObjectiveC里的,它为NSArray添加了许多方法,能让你用流式API来转换、排序、分组和过滤其中的数据。 #import <ReactiveCocoa/RACEXTScope.h> #import <ReactiveCocoa/ReactiveCocoa.h> #import <Social/Social.h> NS_ENUM(NSInteger, RWTwitterInstantError) { RWTwitterInstantErrorAccessDenied, RWTwitterInstantErrorNoTwitterAccounts, RWTwitterInstantErrorInvalidResponse }; static *const RWTwitterInstantDomain = @"TwitterInstant"; RWSearchFormViewController () *searchText; nonatomic) RWSearchResultsViewController *resultsViewController; nonatomic) ACAccountStore *accountStore; ACAccountType *twitterAccountType; RWSearchFormViewController - (self.title @"Twitter Instant"; [self styleTextField:self.searchText]; self.resultsViewController self.splitViewController.viewControllers[1]; self.accountStore = [[ACAccountStore alloc] init]; self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]; [[self.searchText.rac_textSignal map:UIColor yellowColor]; }] subscribeNext:= color; }]; RAC(self.searchText,179)">self.searchText.rac_textSignal map:UIColor yellowColor]; }]; * @weakify: 宏让你创建一个弱引用,如果你需要多个弱引用,你可以传入多个变量。 * @strongify: 让你创建一个对之前传入@weakify对象的强引用。 * then: 方法会等待completed事件的发送,然后再订阅由then block返回的signal,相当于复用subscribeNext:。 这样就高效地把控制权从一个signal传递给下一个。一旦用户允许访问Twitter账号(希望如此), 应用就应该一直监测search text filed的变化,以便搜索Twitter的内容。 * deliverOn: 切换到不同的线程。只能在主线程中更新UI,因此你需要切换线程来在UI中展示微博的列表。 * linq_select: 对数组中的每个元素执行提供的block,来把NSDictionary的数组转换成RWTweet的数组。 * throttle: 节流,前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。 ***************************************************/ @weakify(self); [[[[[[[self requestAccessToTwitterSignal] then:^RACSignal * { @strongify(self); self.searchText.rac_textSignal; }] filter:BOOL(*text) { @strongify(self isValidSearchText:text]; }] throttle:0.5] flattenMap:^RACStream *(self signalForSearchWithText:text]; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:NSDictionary *jsonSearchResult) { NSArray *statuses = jsonSearchResult[@"statuses"]; *tweets = [statuses linq_select:id tweet) { return [RWTweet tweetWithStatus:tweet]; }]; [self.resultsViewController displayTweets:tweets]; } error:NSError *error) { NSLog(@"An error occurred: %@", error); }]; } * 当应用获取访问社交媒体账号的权限时,用户会看见一个弹框。 * 这是一个异步操作,因此把这封装进一个signal。 1. 定义了一个error,当用户拒绝访问时发送。 2. 和第一部分一样,类方法createSignal返回一个RACSignal实例。 3. 通过account store请求访问Twitter。此时用户会看到一个弹框来询问是否允许访问Twitter账户。 4. 在用户允许或拒绝访问之后,会发送signal事件。如果用户允许访问,会发送一个next事件, 紧跟着再发送一个completed事件。如果用户拒绝访问,会发送一个error事件。 requestAccessToTwitterSignal { // 1 - define an error *accessError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorAccessDenied userInfo:nil]; // 2 - create the signal @weakify(self); > subscriber) { // 3 - request access to twitter @strongify(self); [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType options:nil completion:BOOL granted,0); font-weight:bold">*error) { // 4 - handle the response if (!granted) { [subscriber sendError:accessError]; } else { [subscriber sendNext:nil]; [subscriber sendCompleted]; } }]; nil; }]; } * 方法创建了一个请求,请求通过v1.1 REST API来搜索Twitter。 * 下面的代码使用q这个搜索参数来搜索Twitter中包含有给定字符串的微博。 ***************************************************/ - (SLRequest requestforTwitterSearchWithText:(text { NSURL *url NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; *params = @{ @"q" : text }; *request SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:url parameters:params]; return request; } * 基于requestforTwitterSearchWithText这个请求创建signal。 1. 首先需要定义2个不同的错误,一个表示用户还没有添加任何Twitter账号,另一个表示在请求过程中发生了错误。 2. 和之前的一样,创建一个signal。 3. 用你之前写的方法,给需要搜索的文本创建一个请求。 4. 查询account store来找到可用的Twitter账号。如果没有账号的话,发送一个error事件。 5. 执行请求。 6. 在请求成功的事件里(http响应码200),发送一个next事件,返回解析好的JSON数据,然后再发送一个completed事件。 7. 在请求失败的事件里,发送一个error事件。 signalForSearchWithText:(text { // 1 - define the errors *noAccountsError NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorNoTwitterAccounts userInfo:nil]; *invalidResponseError NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorInvalidResponse userInfo:// 2 - create the signal block @weakify(> subscriber) { @strongify(self); // 3 - create the request self requestforTwitterSearchWithText:text]; // 4 - supply a twitter account *twitterAccounts self.accountStore accountsWithAccountType:self.twitterAccountType]; if (twitterAccounts.count == 0) { [subscriber sendError:noAccountsError]; } else { [request setAccount:[twitterAccounts lastObject]]; // 5 - perform the request [request performRequestWithHandler:NSData *responseData, NSHTTPURLResponse *urlResponse,0); font-weight:bold">*error) { if (urlResponse.statusCode 200) { // 6 - on success,parse the response *timelineData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil]; [subscriber sendNext:timelineData]; [subscriber sendCompleted]; } else { // 7 - send an error on failure [subscriber sendError:invalidResponseError]; } }]; } nil; }]; } - (isValidSearchText:(text { 2; } - (styleTextField:(textField { CALayer *textFieldLayer = textField.layer; textFieldLayer.borderColor UIColor grayColor].CGColor; textFieldLayer.borderWidth = 2.0f; textFieldLayer.cornerRadius 0.0f; } RWTweet类有一个属性profileImageUrl来存放头像的URL。为了让table view能流畅地滚动,你需要让用URL获取图像的代码不在主线程中执行。

异步加载图片代码:


// RWSearchResultsViewController.m
// Created by Colin Eberhardt on 03/12/2013. #import "RWTableViewCell.h" RWSearchResultsViewController () strong) *tweets; RWSearchResultsViewController { } - (self.tweets NSArray array]; } - (displayTweets:(tweets { = tweets; [self.tableView reloadData]; } #pragma mark - Table view data source - (NSInteger)tableView:(UITableView tableView numberOfRowsInSection:(NSInteger)section { self.tweets.count; } - (UITableViewCell cellForRowAtIndexPath:(NSIndexPath indexPath { *CellIdentifier @"Cell"; RWTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; RWTweet *tweet self.tweets[indexPath.row]; cell.twitterStatusText.text = tweet.status; cell.twitterUsernameText.text NSString stringWithFormat:@"@%@", tweet.username]; * 因为cell是重用的,可能有脏数据,所以上面的代码首先重置图片。然后创建signal来获取图片数据。 * takeUntil: to cancel a signal when a cell is recycled ***************************************************/ cell.twitterAvatarView.image nil; [[[[self signalForLoadingImage:tweet.profileImageUrl] takeUntil:cell.rac_prepareForReuseSignal] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:UIImage *image) { cell.twitterAvatarView.image = image; }]; return cell; } * 下面的方法首先获取一个后台scheduler,来让signal不在主线程执行。 * 然后,创建一个signal来下载图片数据,当有订阅者时创建一个UIImage。 * 最后是subscribeOn:来确保signal在指定的scheduler上执行。 signalForLoadingImage:(imageUrl { RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]; return [[RACSignal createSignal:> subscriber) { *data NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; *image UIImage imageWithData:data]; [subscriber sendNext:image]; [subscriber sendCompleted]; nil; }] subscribeOn:scheduler]; }

参考链接

  • http://www.infoq.com/cn/articles/reactivecocoa-ios-new-develop-framework
  • http://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1
  • http://benbeng.leanote.com/post/ReactiveCocoaTutorial-part1
  • http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
  • http://benbeng.leanote.com/post/ReactiveCocoaTutorial-part2
  • http://fuckingblocksyntax.com/

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读