ReactiveCocoa(RAC)是一个受函数式响应式编程启发的Cocoa框架。它提供API用于创建和转换随时间变化的值流。
如果您已经熟悉函数式响应式编程或对ReactiveCocoa有所了解,请查看文档部分以获取更多关于其工作方式的详细信息。然后,深入了解我们的文档注释以了解更多关于各个API的信息。
如果您有任何问题,请查看我们的GitHub问题或Stack Overflow上是否有答案。如果没有,请随时提交问题!
此文档记录了RAC 4,目标为Swift 2.2.x
。有关Swift 1.2
的支持,请参阅RAC 3。
非常感谢Rheinfabrik慷慨赞助ReactiveCocoa 3的开发!
ReactiveCocoa受到了函数式响应式编程的启发。而不是使用可变的变量在原地替换和修改,RAC提供“事件流”,由Signal
和SignalProducer
类型表示,这些事件流随时间发送值。
事件流统一了Cocoa中所有用于异步和事件处理的常见模式,包括
NSNotification
(通知)因为这些不同的机制都可以用相同的方式表示,所以我们很容易声明性地将它们串联和组合起来,从而减少了需要桥接的 spaghetti 代码和数据。
有关ReactiveCocoa中概念的更多信息,请参阅框架概述。
假设你有一个文本框,每当用户在其中输入内容时,你想要发起一个网络请求以搜索该查询。
首先,您需要使用RAC对UITextField扩展进行监测,以检测文本框的编辑。
let searchStrings = textField.rac_textSignal()
.toSignalProducer()
.map { text in text as! String }
这会给我们一个发送类型为String值的信号生产者。(当前需要进行强制转换才能将此扩展方法从Objective-C桥接过来。)
对于每一个字符串,我们希望执行一个网络请求。幸运的是,RAC提供了一个用于执行此操作的NSURLSession
扩展。
let searchResults = searchStrings
.flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in
let URLRequest = self.searchRequestWithEscapedQuery(query)
return NSURLSession.sharedSession().rac_dataWithRequest(URLRequest)
}
.map { (data, URLResponse) -> String in
let string = String(data: data, encoding: NSUTF8StringEncoding)!
return self.parseJSONResultsFromString(string)
}
.observeOn(UIScheduler())
这把我们的String生产者转换成了包含搜索结果的Array的生产者,这些结果将通过主线程发送(归功于UIScheduler
)。
此外,这里的flatMap(.Latest)
确保只有一个搜索——最新的——能够运行。如果用户在正在进行的网络请求中输入另一个字符,它将在开始新请求之前被取消。想想看,要手动做这件事需要多少代码!
这实际上还没有执行,因为生产者必须启动才能接收结果(这样当结果未使用时可以避免执行工作)。这很简单
searchResults.startWithNext { results in
print("Search results: \(results)")
}
在这里,我们监听包含我们的结果的Next
事件,并将它们简单地记录到控制台。这很容易做到其他事情,比如更新一个表视图或屏幕上的标签。
到目前为止,示例中任何网络错误都会产生一个事件,这将终止事件流。不幸的是,这还意味着将不会尝试未来的查询。
为了解决这个问题,我们需要决定如何处理发生的失败。最快的解决方案可能是记录它们然后忽略它们。
.flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in
let URLRequest = self.searchRequestWithEscapedQuery(query)
return NSURLSession.sharedSession()
.rac_dataWithRequest(URLRequest)
.flatMapError { error in
print("Network error occurred: \(error)")
return SignalProducer.empty
}
}
通过将失败替换为空事件流,我们实际上能够有效忽略它们。
然而,在放弃之前至少尝试几次可能是更合适的。便捷的是,有一个retry
操作符来做这件事!
我们的改进后的searchResults
生产者可能看起来像这样
let searchResults = searchStrings
.flatMap(.Latest) { (query: String) -> SignalProducer<(NSData, NSURLResponse), NSError> in
let URLRequest = self.searchRequestWithEscapedQuery(query)
return NSURLSession.sharedSession()
.rac_dataWithRequest(URLRequest)
.retry(2)
.flatMapError { error in
print("Network error occurred: \(error)")
return SignalProducer.empty
}
}
.map { (data, URLResponse) -> String in
let string = String(data: data, encoding: NSUTF8StringEncoding)!
return self.parseJSONResultsFromString(string)
}
.observeOn(UIScheduler())
现在,假设你只想在用户停止打字时进行实际搜索,以最小化流量。
ReactiveCocoa有一个可以应用于我们的搜索字符串的声明式throttle
操作符。
let searchStrings = textField.rac_textSignal()
.toSignalProducer()
.map { text in text as! String }
.throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler)
这防止了值在小于0.5秒的时间内被发送,因此用户必须至少停止编辑那么长时间,我们才会使用他们的搜索字符串。
手动执行需要大量的状态,最终读起来也更加困难!在ReactiveCocoa中,我们可以使用单个操作符将时间融入我们的事件流中。
由于其本质,流的堆栈跟踪可能包含成百上千的帧,这通常会使调试变得非常耗时。一种简答的调试方法是向流中注入副作用,如下
let searchString = textField.rac_textSignal()
.toSignalProducer()
.map { text in text as! String }
.throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler)
.on(event: { print ($0) }) // the side effect
这将打印流的事件,同时保留原始流的特性。无论是SignalProducer
还是Signal
都提供了logEvents
操作符,它会自动为您做到这一点
let searchString = textField.rac_textSignal()
.toSignalProducer()
.map { text in text as! String }
.throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler)
.logEvents()
有关更多信息和高阶用法,请查看调试技术文档。
尽管ReactiveCocoa最初是一个Objective-C框架,但自版本3.0起,所有主要功能开发都集中在Swift API上。
RAC的Objective-C API和Swift API是完全分离的,但有一个桥接器可以在两者之间进行转换。这主要用于作为旧ReactiveCocoa项目的兼容层,或者使用尚未添加到Swift API中的Cocoa扩展。
尽管Objective-C API将继续存在并获得支持,但不会得到很多改进。有关如何使用此API的更多信息,请咨询我们的遗留文档。
我们强烈建议所有新项目都使用Swift API。
ReactiveCocoa最初受到了微软的Reactive Extensions (Rx) 库的启发,并受到了很大影响。Rx有许多版本,包括RxSwift,但ReactiveCocoa不是有意进行直接转化的。
在RAC与Rx的不同之处,它通常是
以下是一些具体差异,以及它们的理由。
在大多数版本的Rx中,时间流被称为Observable
,这与.NET中的Enumerable
类型相并行。此外,Rx.NET中的大多数操作都借用了LINQ的名称,这些名称类似关系数据库的术语,如Select
和Where
。
**RAC的首要任务是匹配Swift命名**,使用诸如map
和filter
之类的术语。其他命名差异通常是受Haskell或Elm(“信号”术语的主要来源)的显著更好的替代方案所启发。
Rx中最令人困惑的方面之一是关于“热”,“冷”和“温”可观测对象(事件流)。
简而言之,给定一个像这样仅有的方法或函数声明,在C#中
IObservable<string> Search(string query)
……是无法确定订阅(观察)该IObservable
是否会涉及副作用。如果确实涉及副作用,也无法判断每个订阅是否都有副作用,或者只有第一次订阅才有。
这个例子是故意的,但它演示了一个真实的、普遍存在的问题,使得Rx代码(以及3.0之前的ReactiveCocoa代码)一瞥之下变得特别难以理解。
通过区分副作用,ReactiveCocoa 3.0通过不同的Signal
和SignalProducer
类型来解决这个问题。尽管这又意味着要学习另一种类型,但它提高了代码的可读性,并更好地传达了意图。
换句话说,ReactiveCocoa这里的改变是简单,并非容易。
当在RactiveCocoa中允许信号和信号生产者中失败时,必须在类型系统中指定错误类型。例如,Signal<Int, NSError>
是一个可能以类型为NSError
的错误而失败的有整数值的信号。
更重要的是,RAC允许使用特殊类型NoError
,这静态地保证事件流不允许发送失败。这消除了许多由意外的失败事件引起的错误。
在具有类型的Rx系统中,事件流仅指定其值的类型——而不是其错误的类型,所以这样的保证是不可能的。
Rx基本上对其使用方式漠不关心。尽管用Rx进行UI编程很常见,但它几乎没有为此特定情况量身定制功能。
RAC从ReactiveUI那里汲取了许多灵感,包括动作的基于。
不同的是,ReactiveUI不幸不能直接更改Rx以使其更友好进行UI编程,但ReactiveCocoa已经为此目的进行了多次改进——即使这意味着进一步偏离Rx。
ReactiveCocoa支持OS X 10.9+
,iOS 8.0+
,watchOS 2.0
和tvOS 9.0
。
将RAC添加到您的应用程序中
script/bootstrap
。ReactiveCocoa.xcodeproj
和Carthage/Checkouts/Result/Result.xcodeproj
拖放到您应用程序的Xcode项目或工作区中。ReactiveCocoa.framework
和Result.framework
添加到“嵌入的二进制文件”部分。EMBEDDED_CONTENT_CONTAINS_SWIFT
设置为“是”。请确保将ReactiveCocoa.framework
和Result.framework
都添加到“链接框架和库”和“复制框架”构建阶段中。
如果您希望使用 CocoaPods,有一些第三方慷慨贡献的 非官方的 podspec 供您选择。
一旦您设置了项目,请查看 框架概览 以了解 ReactiveCocoa 的概念,以及 基本运算符 以了解一些使用它的入门示例。
我们还提供了一个非常好的 Playground,让您熟悉 ReactiveCocoa 的运算符。为了开始使用它
script/bootstrap
。ReactiveCocoa.xcworkspace
。Result-Mac
和 ReactiveCocoa-Mac
目标。