ReactiveSwift(简称RAS),好处:
①. 简化响应式函数的模式
在Swift中,我们有几种响应式的开发模式:target-action、代理、通知中心、KVO等。以上每个模式对应的场景不同,我们需要根据场景选择合适的模式。RAS的整合了所有的模式的特点,开辟了一套事件流(信号)-观察者(发送)的模式,将以上种种模式替代。
②.高度聚集的代码实现
我们使用闭包的原因 -- 将相关代码在同一环境中实现异步执行,提升代码的可读性。RAS将这种思想用到极致。
③. 与MVVM设计模式结合使用写出优雅简洁的代码
简化MVC中因项目逐渐庞大导致Controller中代码的沉冗,MVVM模式被我们喜欢,但是它带来的另一个问题是V(View)和VM(viewModel)之间的数据传递过于复杂。而RAS的数据绑定解决了这个问题,我们使用这两者的结合,能够写出很优雅的代码。
RAC 之 Signal
RAC-Signal(信号)就 RAC 来说是构造单元. 它代表我们最终将要收到的信息. 当你能将未来某时刻收到的消息具体表示出来时,你可以开始预先运用逻辑并构建你的信息流,而不是必须等到事件发生(紧迫的).
信号会为了控制通过应用的信息流而获得所有这些异步方法(委托, 回调 block, 通知, KVO, target/action 事件观察, 等)并将它们统一到一个接口下.这只是直观理解. 不仅是这些, 因为信息会流过你的应用, 它还提供给你轻松转换/分解/合并/过滤信息的能力.
信号:
信号是一个发送一连串值的物体. 但是我们这儿的信号啥也不干, 因为它还没有订阅者. 如果有订阅者监听时(已订阅)信号才会发信息. 它将会向那个订阅者发送0或多个载有数值的”next”事件, 后面跟着一个”complete”事件或一个”error”事件. (信号类似于其他语言/工具包中的 “promise”, 但更强大, 因为它不仅限于向它的订阅者一次只传递一个返回值. )
正如我之前提到的, 如果觉得需要的话你可以过滤, 转换, 分解和合并那些值. 不同的订阅者可能需要使用信号通过不同方式发送的值.
信号的由来:
信号是一些等待某事发生的异步代码, 然后把结果值发送给它们的订阅者. 你可以用RAC-Signal的类方法createSignal或者手动创建信号:
在这里用一个具有成功和失败 block (伪造)的网络操作创建了一个信号. (如果我想让信号在被订阅时才让网络请求发生, 还可以用RACSignal的类方法defer. )我在成功的 block 里使用提供的subscriber对象调用sendNext:和sendCompleted:方法, 或在失败的 block 中调用sendError:. 现在我可以订阅这个信号并将在响应返回时接收到 json 值或是 error.
RACSignal *usernameValidSignal = RACObserve(self. viewModel, usernameIsValid);
订阅者:
订阅者就是一段代码, 它等待信号给它发送一些值, 然后订阅者就能处理这些值了. (它也可以作用于 “complete” 和 “error” 事件. )
这有一个简单的订阅者, 是通过向信号的实例方法subscribeNext传入一个 block 来创建的. 我们在这通过RACObserve()宏创建信号来观察一个对象上属性的当前值, 并把它赋值给一个内部属性
- (void) viewDidLoad {
// . . . // create and get a reference to the signal
RACSignal *usernameValidSignal = RACObserve(self. viewModel, isUsernameValid);
// update the local property when this value changes
[usernameValidSignal subscribeNext: ^(NSNumber *isValidNumber) { self. usernameIsValid = isValidNumber. boolValue }];}
注意: RAC 只处理对象, 而不处理像BOOL这样的原始值. 不过不用担心, RAC 通常会帮你这些转换.
幸运的是 RAC 的创造者也意识到这种绑定行为的普遍必要性, 所以他们提供了另一个宏RAC(). 与RACObserve()相同, 你提供想要与即将到来的值绑定的对象和参数, 在其内部它所做的是创建一个订阅者并更新其属性的值. 我们的例子现在看起来像这样:
- (void) viewDidLoad {
RAC(self, usernameIsValid) = RACObserve(self. viewModel, isUsernameValid);
}
转换数据流
RAC 为我们提供的用于转换数值流的方法. 我们将会利用RACSignal的实例方法map.
我们将 view-model 上的isUsernameValid发生的变化直接绑定到goButton的enabled属性上. 对alpha的绑定更酷, 因为我们正在使用map方法将值转换成与alpha属性相关的值. (注意在这里我们返回的是一个NSNumber对象而不是原始float值. 这基本上是唯一的污点: 你需要负责为 RAC 将原始值转化为对象, 因为它不能帮你导出来.
订阅信号链时要明白重要的一件事是每当一个新值通过信号链被发送出去时, 实际上会给每个订阅者都发送一次. 直到意识到这就我们而言是有意义的, 信号发出的值不存储在任何地方(除了 RAC 在内部实现中). 当信号需要发送一个新的值时, 它会遍历所有的订阅者并给每个订阅者发送那个值.
这为什么重要?这意味着信号链某处存在的任何副作用, 任何影响应用世界的转变, 将会发生多次. 这对新接触 RAC 的用户来说是意想不到的. (这也违反了函数式构建的理念-数据输入, 数据输出).
一个做作的例子可能是: 信号链某处的信号在每次按钮被按下时更新self中的一个计数器属性. 如果信号链有多个订阅者, 计数器的增长将会比你想的还要多. 你需要努力从信号链中尽可能剔除副作用. 当副作用不可避免时, 你可以使用一些恰当的预防机制
除副作用之外, 你需要注意带有昂贵操作和可变数据的信号链. 网络请求就是一个三者兼得的例子:
a. 网络请求影响了应用的网络层(副作用).
b. 网络请求为信号链引入了可变数据. (两个完全一样请求可能返回了不同的数据. )
c. 网络请求反应慢
Tweetboat Plus
让我们着眼于如何用 ReactiveCocoa 将 view-model 与视图控制器连接起来:
- (void) viewDidLoad {
[super viewDidLoad];
RAC(self. viewModel, username) = [myTextfield rac_textSignal];
RACSignal *usernameIsValidSignal = RACObserve(self. viewModel, usernameValid);
RAC(self. goButton, alpha) = [usernameIsValidSignal map: ^(NSNumber *valid) { return valid. boolValue ? @1 : @0. 5; }];
RAC(self. goButton, enabled) = usernameIsValidSignal;
RAC(self. avatarImageView, image) = RACObserve(self. viewModel, userAvatarImage);
RAC(self. userNameLabel, text) = RACObserve(self. viewModel, userFullName);
@weakify(self);
[[[RACSignal merge: @[RACObserve(self. viewModel, tweets), RACObserve(self. viewModel, allTweetsLoaded)]] bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]] subscribeNext: ^(id value) { @strongify(self); [self. tableView reloadData]; }];
[[self. goButton rac_signalForControlEvents: UIControlEventTouchUpInside] subscribeNext: ^(id value) { @strongify(self); [self. viewModel getTweetsForCurrentUsername]; }];
}
-(UITableViewCell*)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
// if table section is the tweets section
if (indexPath. section == 0) { MYTwitterUserCell *cell = [self. tableView dequeueReusableCellWithIdentifier: @"MYTwitterUserCell" forIndexPath: indexPath];
// grab the cell view model from the vc view model and assign it
cell. viewModel = self. viewModel. tweets[indexPath. row];
return cell;
} else {
// else if the section is our loading cell
MYLoadingCell *cell = [self. tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self. viewModel loadMoreTweets];
return cell;
}
}
// MYTwitterUserCell
// this could also be in cell init
- (void) awakeFromNib {
[super awakeFromNib];
RAC(self. avatarImageView, image) = RACObserve(self, viewModel. tweetAuthorAvatarImage);
RAC(self. userNameLabel, text) = RACObserve(self, viewModel. tweetAuthorFullName);
RAC(self. tweetTextLabel, text) = RACObserve(self, viewModel. tweetContent);
}
RAC(self.viewModel, username) = [myTextfield rac_textSignal];
在这我们用 RAC 库中的方法从UITextField拉取一个信号. 这行代码将 view-model 上的可读写属性username绑定到文本框上的用户输入的任何更新.
RACSignal *usernameIsValidSignal = RACObserve(self. viewModel, usernameValid);
RAC(self. goButton, alpha) = [usernameIsValidSignal map: ^(NSNumber *valid) { return valid. boolValue ? @1 : @0. 5; }];
RAC(self. goButton, enabled) = usernameIsValidSignal;
在这我们用RACObserve方法在 view-model 的usernameValid属性上创建了一个信号usernameIsValidSignal. 无论何时属性发生变化, 它将会沿着管道发送一个新的@YES或@NO. 我们拿到那个值并将其绑定到goButton的两个属性上. 首先我们将alpha分别对应 YES 或 NO 更新到1或0. 5(记着在这必须返回NSNumber). 然后我们直接将信号绑定到enabled属性, 因为 YES 和 NO 在这无需转换就能完美地运作.
RAC(self. avatarImageView, image) = RACObserve(self. viewModel, userAvatarImage);
RAC(self. userNameLabel, text) = RACObserve(self. viewModel, userFullName);
参考资料: ReactiveCocoa和MVVM入门