ReactiveCocoa入门教程:第一部分

爬虫网易易盾滑块及轨迹算法案例:知乎(附轨迹生成源码) 声明:该文章为学习使用,严禁用于商业用途和非法用途,违者后果自负,由此产生的一切后果均与作者无关。 阅读详情

本文翻译自RayWenderlich,原文:ReactiveCocoa Tutorial--The Definitive Introduction: Part 1/2

作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

如果你对上面说的还比较疑惑,那还是继续往下看吧。

ReactiveCocoa结合了几种编程风格:

函数式编程(Functional Programming):使用高阶函数,例如函数用其他函数作为参数。

响应式编程(Reactive Programming):关注于数据流和变化传播。

所以,你可能听说过ReactiveCocoa被描述为函数响应式编程(FRP)框架。

这就是这篇教程要讲的内容。编程范式是个不错的主题,但是本篇教程的其余部分将会通过一个例子来实践。 

Reactive Playground

通过这篇教程,一个简单的范例应用Reactive Playground,你将会了解到响应式编程。下载初始工程,然后编译运行一下确保你已经把一切都设置正确了。

Reactive Playground是一个非常简单的应用,它为用户展示了一个登录页。在用户名框输入user,在密码框输入password,然后你就能看到有一只可爱小猫咪的欢迎页了。

01.jpg

呀,真是可爱啊。

现在可以花一些时间来看一下初始工程的代码。很简单,用不了多少时间。

打开RWViewController.m看一下。你多快能找到控制登录按钮是否可用的条件?判断显示/隐藏登录失败label的条件是什么?在这个相对简单的例子里,可能只用一两分钟就能回答这些问题。但是对于更复杂的例子,这些所花的时间可能就比较多了。

使用ReactiveCocoa,可以使应用的基本逻辑变得相当简洁。是时候开始啦。

添加ReactiveCocoa框架

添加ReactiveCocoa框架最简单的方法就是用CocoaPods。如果你从没用过CocoaPods,那还是先去看看CocoaPods简介这篇教程吧。请至少看完教程中初始化的步骤,这样你才能安装框架。

注意:如果不想用CocoaPods,你仍然可以使用ReactiveCocoa,具体查看Github文档中引入ReactiveCocoa的步骤描述。

译注:我就是不喜欢用CocoaPods的那波人。所以我首先使用了Github上提供的方法,但是在第二步执行bootstrap时提示缺少xctool,我就果断放弃了,还是乖乖用CocoaPods吧。

具体怎么使用CocoaPods安装就不详细讲解了。 

开始

就像在介绍中提到的,RAC为应用中发生的不同事件流提供了一个标准接口。在ReactiveCocoa术语中这个叫做信号(signal),由RACSignal类表示。

打开应用的初始view controller,RWViewController.m ,引入ReactiveCocoa的头文件。

1
#import

不要替换已有的代码,将下面的代码添加到viewDidLoad方法的最后:

1
2
3
[self.usernameTextField.rac_textSignal subscribeNext:^(id x){
   NSLog(@ "%@" , x);
}];

 编译运行,在用户名输入框中输几个字。注意console的输出应该和下面的类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i
2013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is 
2013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t
2013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th
2013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi
2013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is  this
2013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is  this 
2013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is  this  m
2013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is  this  ma
2013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is  this  mag
2013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is  this  magi
2013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is  this  magic
2013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is  this  magic?

可以看到每次改变文本框中的文字,block中的代码都会执行。没有target-action,没有delegate,只有signal和block。令人激动不是吗?

ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。在本教程的第一部分,我们将会关注next事件。在第二部分,将会学习error和completed事件。

RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,subscribeNext:方法提供的block都会执行。

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

原理就说这么多,是时候开始让ReactiveCocoa干活了。

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

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

编译运行,在text field只能怪输入几个字,你会发现只有当输入超过3个字符时才会有log。

1
2
3
4
5
6
7
8
9
10
11
2013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t
2013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th
2013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi
2013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is  this
2013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is  this 
2013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is  this  m
2013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is  this  ma
2013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is  this  mag
2013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is  this  magi
2013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is  this  magic
2013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is  this  magic?

刚才所创建的只是一个很简单的管道。这就是响应式编程的本质,根据数据流来表达应用的功能。

用图形来表达就是下面这样的:

FilterPipeline.png

从上面的图中可以看到,rac_textSignal是起始事件。然后数据通过一个filter,如果这个事件包含一个长度超过3的字符串,那么该事件就可以通过。管道的最后一步就是subscribeNext:,block在这里打印出事件的值。

filter操作的输出也是RACSignal,这点先放到一边。你可以像下面那样调整一下代码来展示每一步的操作。

1
2
3
4
5
6
7
8
9
10
11
12
RACSignal *usernameSourceSignal =
     self.usernameTextField.rac_textSignal;
  
RACSignal *filteredUsername =[usernameSourceSignal
   filter:^BOOL(id value){
     NSString*text = value;
     return  text.length > 3;
   }];
  
[filteredUsername subscribeNext:^(id x){
   NSLog(@ "%@" , x);
}];

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

注意:ReactiveCocoa大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.com。

类型转换

如果你之前把代码分成了多个步骤,现在再把它改回来吧。

1
2
3
4
5
6
7
8
[[self.usernameTextField.rac_textSignal
   filter:^BOOL(id value){
     NSString*text = value;  // implicit cast
     return  text.length > 3;
   }]
   subscribeNext:^(id x){
     NSLog(@ "%@" , x);
   }];

在上面的代码中,注释部分标记了将id隐式转换为NSString,这看起来不是很好看。幸运的是,传入block的值肯定是个NSString,所以你可以直接修改参数类型,把代码更新成下面的这样的:

1
2
3
4
5
6
7
[[self.usernameTextField.rac_textSignal
   filter:^BOOL(NSString*text){
     return  text.length > 3;
   }]
   subscribeNext:^(id x){
     NSLog(@ "%@" , x);
   }];

编译运行,确保没什么问题。

什么是事件呢?

到目前为止,本篇教程已经描述了不同的事件类型,但是还没有说明这些事件的结构。有意思的是(?),事件可以包括任何事情。

下面来展示一下,在管道中添加另一个操作。把添加在viewDidLoad中的代码更新成下面的:

1
2
3
4
5
6
7
8
9
10
[[[self.usernameTextField.rac_textSignal
   map:^id(NSString*text){
     return  @(text.length);
   }]
   filter:^BOOL(NSNumber*length){
     return [length integerValue] > 3;
   }]
   subscribeNext:^(id x){
     NSLog(@ "%@" , x);
   }];

编译运行,你会发现log输出变成了文本的长度而不是内容。

1
2
3
4
5
6
7
8
9
2013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4
2013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5
2013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6
2013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7
2013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8
2013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9
2013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10
2013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11
2013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12

新加的map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber。

来看下面的图片:

FilterAndMapPipeline.png

能看到map操作之后的步骤收到的都是NSNumber实例。你可以使用map操作来把接收的数据转换成想要的类型,只要它是个对象。

注意:在上面的例子中text.length返回一个NSUInteger,是一个基本类型。为了将它作为事件的内容,NSUInteger必须被封装。幸运的是Objective-C literal syntax提供了一种简单的方法来封装——@ (text.length)。

现在差不多是时候用所学的内容来更新一下ReactivePlayground应用了。你可以把之前的添加代码都删除了。

创建有效状态信号

首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。把下面的代码添加到RWViewController.m中viewDidLoad的最后面:

1
2
3
4
5
6
7
8
9
10
RACSignal *validUsernameSignal =
  [self.usernameTextField.rac_textSignal
  map:^id(NSString *text) {
  return  @([self isValidUsername:text]);
  }]; 
RACSignal *validPasswordSignal =
  [self.passwordTextField.rac_textSignal 
  map:^id(NSString *text) { 
  return  @([self isValidPassword:text]);
  }];

可以看到,上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber封装的布尔值。

下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法:

1
2
3
4
5
6
7
[[validPasswordSignal
   map:^id(NSNumber *passwordValid){
     return [passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
   }]
   subscribeNext:^(UIColor *color){
     self.passwordTextField.backgroundColor = color;
   }];

(不要使用这段代码,下面有一种更好的写法!)

从概念上来说,就是把之前信号的输出应用到输入框的backgroundColor属性上。但是上面的用法不是很好。

幸运的是,ReactiveCocoa提供了一个宏来更好的完成上面的事情。把下面的代码直接加到viewDidLoad中两个信号的代码后面:

1
2
3
4
5
6
7
8
9
10
11
RAC(self.passwordTextField, backgroundColor) =
   [validPasswordSignal
     map:^id(NSNumber *passwordValid){
       return [passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
     }];
  
RAC(self.usernameTextField, backgroundColor) =
   [validUsernameSignal
     map:^id(NSNumber *passwordValid){
      return [passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
    }];

RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

你不觉得这种方法很好吗?

在编译运行之前,找到updateUIState方法,把头两行删掉。

1
2
3
4
self.usernameTextField.backgroundColor = 
     self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor]; 
self.passwordTextField.backgroundColor = 
     self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];

这样就把不相关的代码删掉了。

编译运行,可以发现当输入内容无效时,输入框看起来高亮了,有效时又透明了。

现在的逻辑用图形来表示就是下面这样的。能看到有两条简单的管道,两个文本信号,经过一个map转为表示是否有效的布尔值,再经过一个map转为UIColor,而这个UIColor已经和输入框的背景颜色绑定了。

TextFieldValidPipeline.png

你是否好奇为什么要创建两个分开的validPasswordSignal和validUsernameSignal呢,而不是每个输入框一个单独的管道呢?(?)稍安勿躁,答案就在下面。

原文:Are you wondering why you created separate validPasswordSignal and validUsernameSignal signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!

聚合信号

目前在应用中,登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。

现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了——validUsernameSignal和validPasswordSignal了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。

把下面的代码添加到viewDidLoad的末尾:

1
2
3
4
5
RACSignal *signUpActiveSignal =
   [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                     reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
                       return  @([usernameValid boolValue]&&[passwordValid boolValue]);
                     }];

上面的代码使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。

注意:RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。

现在已经有了合适的信号,把下面的代码添加到viewDidLoad的末尾。这会把信号和按钮的enabled属性绑定。

1
2
3
[signUpActiveSignal subscribeNext:^(NSNumber*signupActive){
    self.signInButton.enabled =[signupActive boolValue];
  }];

在运行之前,把以前的旧实现删掉。把下面这两个属性删掉。

1
2
@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;

把viewDidLoad中的这些也删掉:

1
2
3
4
5
6
7
// handle text changes for both text fields
[self.usernameTextField addTarget:self
                            action:@selector(usernameTextFieldChanged)
                  forControlEvents:UIControlEventEditingChanged];
[self.passwordTextField addTarget:self
                            action:@selector(passwordTextFieldChanged)
                 forControlEvents:UIControlEventEditingChanged];

同样把updateUIState、usernameTextFieldChanged和passwordTextFieldChanged方法删掉。

最后确保把viewDidLoad中updateUIState的调用删掉。

编译运行,看看登录按钮。当用户名和密码输入有效时,按钮就是可用的,和以前一样。

现在应用的逻辑就是下面这样的:

CombinePipeline.png

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

  • 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。

  • 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。

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

响应式的登录

应用目前使用上面图中展示的响应式管道来管理输入框和按钮的状态。但是按钮按下的处理用的还是action,所以下一步就是把剩下的逻辑都替换成响应式的。

在storyboard中,登录按钮的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是绑定的。下面会用响应的方法替换,所以首先要做的就是断开当前的storyboard action。

打开Main.storyboard,找到登录按钮,按住ctrl键单击,打开outlet/action连接框,然后点击x来断开连接。如果你找不到的话,下图中红色箭头指示的就是删除按钮。

DisconnectAction.jpg

你已经知道了ReactiveCocoa框架是如何给基本UIKit控件添加属性和方法的了。目前你已经使用了rac_textSignal,它会在文本发生变化时产生信号。为了处理按钮的事件,现在需要用到ReactiveCocoa为UIKit添加的另一个方法,rac_signalForControlEvents。

现在回到RWViewController.m,把下面的代码添加到viewDidLoad的末尾:

1
2
3
4
5
[[self.signInButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    subscribeNext:^(id x) {
      NSLog(@ "button clicked" );
    }];

上面的代码从按钮的UIControlEventTouchUpInside事件创建了一个信号,然后添加了一个订阅,在每次事件发生时都会输出log。

编译运行,确保的确有log输出。按钮只在用户名和密码框输入有效时可用,所以在点击按钮前需要在两个文本框中输入一些内容。

可以看到Xcode控制台的输出和下面的类似:

1
2
3
4
5
2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked

现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。那么问题就来了,打开RWDummySignInService.h,看一下接口:

1
2
3
4
5
6
7
8
9
typedef void (^RWSignInResponse)(BOOL);
  
@interface RWDummySignInService : NSObject
  
- (void)signInWithUsername:(NSString *)username
                   password:(NSString *)password
                   complete:(RWSignInResponse)completeBlock;
                   
@end

这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的subscribeNext: blcok里直接调用这个方法,但是为什么你要这么做?(?)

注意:本教程为了简便使用了一个假的service,所以它不依赖任何外部API。但你现在的确遇到了一个问题,如何使用这些不是用信号表示的API呢?

创建信号

幸运的是,把已有的异步API用信号的方式来表示相当简单。首先把RWViewController.m中的signInButtonTouched:删掉。你会用响应式的的方法来替换这段逻辑。

还是在RWViewController.m中,添加下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (RACSignal *)signInSignal {
return  [RACSignal createSignal:^RACDisposable *(id subscriber){
    [self.signInService 
      signInWithUsername:self.usernameTextField.text
                password:self.passwordTextField.text
                complete:^(BOOL success){
                     [subscriber sendNext:@(success)];
                     [subscriber sendCompleted];
      }];
    return  nil;
}];
}

上面的方法创建了一个信号,使用用户名和密码登录。现在分解来看一下。

上面的代码使用RACSignal的createSignal:方法来创建信号。方法的入参是一个block,这个block描述了这个信号。当这个信号有subscriber时,block里的代码就会执行。

block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。

这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。

可以看到,把一个异步API用信号封装是多简单!

现在就来使用这个新的信号。把之前添加在viewDidLoad中的代码更新成下面这样的:

1
2
3
4
5
6
7
8
[[[self.signInButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    map:^id(id x){
      return [self signInSignal];
    }]
    subscribeNext:^(id x){
      NSLog(@ "Sign in result: %@" , x);
    }];

上面的代码使用map方法,把按钮点击信号转换成了登录信号。subscriber输出log。

编译运行,点击登录按钮,查看Xcode的控制台,等等,输出的这是个什么鬼?

1
2
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign  in  result:
                                     name: +createSignal:

没错,你已经给subscribeNext:的block传入了一个信号,但传入的不是登录结果的信号。

下图展示了到底发生了什么:

02.png

当点击按钮时,rac_signalForControlEvents发送了一个next事件(事件的data是UIButton)。map操作创建并返回了登录信号,这意味着后续步骤都会收到一个RACSignal。这就是你在subscribeNext:这步看到的。

上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。

信号中的信号

解决的方法很简单,只需要把map操作改成flattenMap就可以了:

1
2
3
4
5
6
7
8
[[[self.signInButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    flattenMap:^id(id x){
      return [self signInSignal];
    }]
    subscribeNext:^(id x){
      NSLog(@ "Sign in result: %@" , x);
    }];

这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。

编译运行,注意控制台,现在应该输出登录是否成功了。

1
2
2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign  in  result: 0
2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign  in  result: 1

还不错。

现在已经完成了大部分的内容,最后就是在subscribeNext步骤里添加登录成功后跳转的逻辑。把代码更新成下面的:

1
2
3
4
5
6
7
8
9
10
11
12
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
    return [self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
    BOOL success =[signedIn boolValue];
    self.signInFailureText.hidden = success;
    if (success){
      [self performSegueWithIdentifier:@ "signInSuccess"  sender:self];
    }
   }];

subscribeNext: block从登录信号中取得结果,相应地更新signInFailureText是否可见。如果登录成功执行导航跳转。

编译运行,应该就能再看到可爱的小猫啦!喵~

03.jpg

你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。

这个逻辑应该怎么添加呢?改变按钮的可用状态并不是转换(map)、过滤(filter)或者其他已经学过的概念。其实这个就叫做“副作用”,换句话说就是在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

添加附加操作(Adding side-effects)

把代码更新成下面的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[[[self.signInButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    doNext:^(id x){
      self.signInButton.enabled =NO;
      self.signInFailureText.hidden =YES;
    }]
    flattenMap:^id(id x){
      return [self signInSignal];
    }]
    subscribeNext:^(NSNumber*signedIn){
      self.signInButton.enabled =YES;
      BOOL success =[signedIn boolValue];
      self.signInFailureText.hidden = success;
      if (success){
        [self performSegueWithIdentifier:@ "signInSuccess"  sender:self];
      }
    }];

你可以看到doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。

上面的doNext: block把按钮置为不可点击,隐藏登录失败提示。然后在subscribeNext: block里重新把按钮置为可点击,并根据登录结果来决定是否显示失败提示。

之前的管道图就更新成下面这样的:

04.png

编译运行,确保登录按钮的可点击状态和预期的一样。

现在所有的工作都已经完成了,这个应用已经是响应式的啦。

如果你中途哪里出了问题,可以下载最终的工程(依赖库都有),或者在Github上找到这份代码,教程中的每一次编译运行都有对应的commit。

注意:在异步操作执行的过程中禁用按钮是一个常见的问题,ReactiveCocoa也能很好的解决。RACCommand就包含这个概念,它有一个enabled信号,能让你把按钮的enabled属性和信号绑定起来。你也许想试试这个类。

总结

希望本教程为你今后在自己的应用中使用ReactiveCocoa打下了一个好的基础。你可能需要一些练习来熟悉这些概念,但就像是语言或者编程,一旦你夯实基础,用起来也就很简单了。ReactiveCocoa的核心就是信号,而它不过就是事件流。还能再更简单点吗?

在使用ReactiveCocoa后,我发现了一个有趣的事情,那就是你可以用很多种不同的方法来解决同一个问题。你可以用教程中的例子试试,调整一下信号,改改信号的分割和聚合。

ReactiveCocoa的主旨是让你的代码更简洁易懂,这值得多想想。我个人认为,如果逻辑可以用清晰的管道、流式语法来表示,那就很好理解这个应用到底干了什么了。

在本系列教程的第二部分,你将会学到诸如错误处理、在不同线程中执行代码等高级用法。

Linux下科大讯飞离线语音识别SDK从下载到实战(附常见问题解决) 本文详细介绍了在Linux系统下,如何从科大讯飞开放平台获取离线语音识别SDK,完成环境准备、编译部署到实战开发的完整流程。文章重点解析了SDK目录结构、语法词典定制、示例代码修改,并针对开发中常见的“词典加载失败”、音频格式、交叉编译及性能优化等问题提供了深度解决方案,助力嵌入式与IoT开发者快速实现稳定可靠的离线命令识别功能。 阅读详情

相关推荐

RK3588 InsightFace人脸识别移植及精度测试全解析

本文详细介绍了如何在RK3588平台上移植InsightFace人脸识别模型并进行精度验证。通过PyTorch转ONNX、ONNX转RKNN量化模型的关键步骤,实现了INT8量化模型的部署。测试数据显示,该模型在LFW数据集上达到99.43%的准确率(接近官方99.52%),推理时间仅14.23ms。ROC曲线和相似度分布图表明模型区分能力优秀,AUC超过0.99,满足实际应用需求。文中还详解了量化原理、环境搭建和测试方法,为边缘设备部署人脸识别提供了完整解决方案。

Hi20240217的博客 3673

网易易盾滑块逆向分析 js 滑动轨迹生成_2

1、首先就是通过我第一篇文章拿到我们要滑动的图片,还有token2、第二步识别出滑动的距离,这里考虑到图片中滑块距离左边的距离需要在识别出来的距离上适当的加上或者减去一些数3、通过识别出来的距离以及token去模拟滑动,通过 x 还有移动的距离去随机生成并进行加密4、接着就是 d m p ext 的生成,这里需要助理的是图片的大小,是否是300 或者320 适当的进行调整,还有距离左边的距离,都是需要去测试的。

weixin_44236034的博客 3807

利用VBA宏实现LightTools仿真数据自动化导出至Excel

本文详细介绍了如何利用VBA宏实现LightTools仿真数据自动化导出至Excel的完整流程。通过调用LTAPI接口,工程师可以自动执行仿真、抓取照度与光强等数值数据、捕获并粘贴仿真图像,从而将重复性工作自动化,显著提升光学设计工作的效率和准确性。

weixin_28314575的博客 502

新版网易滑块

网易新版滑块验证

qq_44657571的博客 940

Hopfield神经网络(HNN)详解

Hopfield神经网络是一种比较特殊的网络,它不像一般的神经网络那样有输入层和输出层,并且通过训练来改变神经网络中的参数,最终实现预测、识别等功能。Hopfield网络只有一群神经元节点,所有节点之间相互连接,形式如下: 虽然有些文章会画出像深度神经网络那样有输入输出的图,并且输出后又返回输入,但本质上就是多个节点之间的全连接,也就是上面的图形。Hopfield神经网络主要有两个应用:一是起到类似储存器的作用,也就是我们把多个序列或图片输入这个网络,最终这个网络会以神经元之间连接权重的形式储存这

weixin_60737527的博客 4万+

几何修复_ANSYS流体分析:几何前处理

建模及几何处理工具非常多,选择起来让人眼花缭乱。对于仿真而言,高效的对几何进行清理及生成符合仿真要求的几何至关重要。ANSYS SpaceClaim Direct Modeler是基于直接建模思想的新一代3D高效率建模和几何编辑修补软件。SCDM提供了当今最快速、最富有创意的直接建模解决方案。由于它的直接建模能力即使对于导入的其它3D模型(如catia、proe、solid works创建的几何)...

weixin_35778830的博客 2418

【web js逆向分析易盾滑块fp参数】逆向分析网易易盾滑块的 fp 参数,仅供学习交流

【web js逆向分析易盾滑块fp参数】逆向分析网易易盾滑块的 fp 参数,仅供学习交流。 看完你就知道怎么玩了,全程不废话,看到就是学到

₰₯₮ 的博客 3279

AI系统架构设计实战:AI应用架构师的深度指南

核心概念:AI系统架构是指设计和组织人工智能系统各个组件(包括数据、算法、计算资源、集成接口等)的结构框架,以及这些组件之间的交互方式和原则。它为AI应用提供了技术蓝图,决定了系统的性能、可扩展性、可维护性和可靠性。数据的本质:数据是对客观事物的符号表示,是可识别的、抽象的符号。在AI系统中,数据不仅是输入,也是系统学习和改进的基础。数据就像AI系统的"燃料",其质量和数量直接决定了AI系统的能力边界。

AI天才研究院 1199

网易易盾滑块逆向

接下来考虑atomtraceData的生成,下面是我的代码(基本和之前一样,不多赘述),毕竟滑块轨迹的生成的基本上不是过滑块的重点,还是用打码平台(得塔云)这样traceData,atomtraceData都有了(注意这里的x_end不是js中的,js中的x_end,要再减7),最后来解决data的生成。发现它导出的是fC,而fC中并没有直接导出f3这个函数,所以我选择魔改一下源码(有风险,比如别的地方调用index=71的函数,它只返回f3函数)注意看,这就是第一第二次发送的请求,我们的中心在第二次。

2401_88301759的博客 5560

28-基于STM32单片机的便携式健康监测系统设计与实现(程序+原理图+元件清单)

本文详细介绍了基于STM32单片机的便携式健康监测系统设计与实现,该系统集成MAX30102光电传感器和MSP20压力传感器,实时监测心率、血氧和血压等关键生理指标。通过STM32F103C8T6主控芯片和HC-05蓝牙模块,实现数据采集、处理及无线传输,具有便携性高、操作简便等特点,适用于家庭健康监护和户外运动场景。

weixin_29191669的博客 420

基于STM32设计的4G远程视频侦查小车(图传+远程控制+EMQX+MQTT)(204)

当前设计了一款基于STM32F103ZET6主控芯片和废旧Android手机改造的4G侦查小车。该设计充分利用已有的智能手机硬件资源,实现摄像头画面的实时采集与上传,以及远程控制功能。这样的设计不仅降低了成本,而且充分利用了废旧手机中高性能的硬件资源,如摄像头、4G通信模块和电池等,实现了资源的有效循环利用。

2534

ReactiveCocoa入门教程——第一部分

本文翻译自RayWenderlichReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2 作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过K...

chunqingtai2922的博客 112

液态神经网络(LNN):打破AI“静态快照”的连续动力学革命---AI大模型系统从零开始0029

MIT研究团队受秀丽隐杆线虫神经系统的启发,开发出革命性的液态神经网络(LNN)。该技术通过连续微分方程建模神经元动态,实现输入响应速度的自适应调节,解决了传统AI模型静态化、高能耗和低解释性的痛点。LNN具有三大突破性优势:仅需少量神经元(如20个)即可完成复杂任务;能处理非均匀时序数据;在自动驾驶、医疗监测等领域展现出卓越的适应性和鲁棒性。这项将生物智能与数学建模完美融合的技术,有望成为下一代AI的核心架构,目前已在无人机导航等场景验证了其高效性,被预测为2026年最具潜力的AI突破方向。

添柴程序猿的专栏 376

ReactiveCocoa入门教程——第一部分【转载】

作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。 如果你对上...

dianye0094的博客 150

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

上一篇: Xcode报错提示:“Info.plist couldn't be opened because there is no such file”
下一篇: RAC中经常使用的运算符 - 感谢听榆大叔的整理
kintan哥
博客等级 码龄12年 11粉丝 32原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值