这篇文章将纪录用Swift语言配合ReactiveCocoa写一个伪搜索引擎app的历程。
大量参考了RayWenderlich.com上的文章(原文连接1 原文连接2)。原文是针对Objective C的,可是如今Swift都已经更新到了3.0(虽然由于做者没有developer id,用的仍是2.2),ReactiveCocoa也更新到了4.2,原来的大多数技术都已经不能直接使用了(ReactiveCocoa的开发者甚至说在将来的5.0版本中要移除Objective C的支持)。做者为此也走过很多弯路,参考了多方资料,最终完成了这个sample app,但愿可让别人少走一点弯路。
做者推荐新接触iOS的开发者在学完基础的swift以后直接使用ReactiveCocoa+Swift来写代码,这将节省大量的精力。react
做为iOS开发者,咱们写的每一行代码几乎都是在和“事件”打交道,例如用户点击了一个按钮,网络上发来一条信息,一个属性值的变化(Key Value Observation),或者是用户的位置改变了。可是,CocoaTouch把这些事件以不一样的格式封装在了一块儿,例如target-action, delegate, KVO, 回调之类的。这就给开发带来了很大的麻烦,下降了代码的可读性,随之而来的就是更多的维护成本和更多的bug。ReactiveCocoa把这一切封装到了一个标准的接口中,这样它们就能够很容易地被组合、过滤。git
ReactiveCocoa把函数式编程和响应式编程组合在了一块儿。github
函数式编程:这种编程方式使用了高阶函数,也就是把其余函数做为参数的函数(做者认为RayWenderlich.com的解释不太好。做者认为函数式编程就是一个不一样的计算机架构方式。它注重数学在计算机科学中发挥的做用,把函数理解成真正的数学上的函数。其核心就是没有Side Effect,也没有变量。这很是好的避免了并发编程中的不少问题,于是在这两年逐渐流行。Swift中有良好的函数式编程支持,其语法对著名而常用的Monad结构的支持甚至比经典的Haskell语言还好)编程
响应式编程:这种编程方式注重数据流的传播和管道式的程序结构。这种结构也是为了复杂的并发程序而生的,先天具备简洁、安全的特色。swift
由于这个缘由,ReactiveCocoa也被称为是一个函数响应式编程框架。设计模式
这里就再也不在学术上深究了,打开Xcode吧!xcode
程序最终运行的效果以下:安全
当用户在文本框中输入长度大于4的文本时,下方的列表就会显示和用户输入字符长度相同的“搜索结果”(为了保持简单,这里就直接生成了一些字符串,而不是去调用搜索引擎的API)。而且只有当用户的输入在0.5秒中没有变化时,动做才会被触发。因为很是简单,不考虑错误处理。因为真正的Web API须要访问网络,引起异步事件,这个程序若是使用普通的方法将具备至关的复杂性。网络
程序内部将采用管道式,数据流通过管道以后最终将被一个UITableView显示。因为Swift中变量绑定的问题,程序并未采用MVVM设计模式,代码中的ViewModel只是一个保存数据的容器。(将来将会改进为MVVM模式)闭包
创建一个iOS工程,类型是Single View Application,设备选择iPhone(方便UI设计),语言选择swift
ReactiveCocoa官方推荐使用Carthage安装。(固然CocoaPod用不了,缘由你懂的)Carthage安装外部库的操做很是简单:
打开终端,定位到工程的根目录下(即*.xcodeproj所在的地方),使用文本编辑器创建一个Cartfile
在终端输入nano Cartfile
在新创建的文件中加入一行github "ReactiveCocoa/ReactiveCocoa"
Control-O保存,Control-X离开 回到终端,输入命令carthage update
等待Carthage下载并编译框架 (若是还没有安装Carthage,能够到官网下载二进制文件,或者用Homebrew:brew install carthage
)
打开工程文件,在General选项卡下加入刚刚编译出的framework文件(注意到还有一个Result.framework):
在Build Phases选项卡下加入一个新的Run Script,并添加文件,最终看起来应该是这样(命令须要手工输入 若是不说Homebrew安装的Carthage,路径可能不同):
如今框架已经引入完了,能够试验一下,到ViewController.swift中输入import R
,Xcode的自动补全应当在这时给出ReactiveCocoa
的提示,那就说明安装完成了
UITableView是UIKit中操做较为复杂的一个,但这个特性也让它能够不须要绑定就直接使用ReactiveCocoa的特性,所以在这里选用它来作介绍。
不了解UITableView的操做并不影响接下去的阅读,由于全部操做都被说明了
首先打开Main.storyboard,向场景中拖入一个Text Field。设置AutoLayout:左20,右20,上0。再拖入一个Table View,放在Text Field下方,设置AutoLayout:左20,右20,下20,上8。再拖入一个Table View Cell放在Table View里面,拖入一个Label放在Cell里面,设置AutoLayout:竖直居中,左50。整个场景看上去应该像这样:
以后,在StoryBoard中设置Cell的identifier为ResultCell。咱们将在以后编写完Cell的代码以后改变这个Cell的其余属性。如图:
接下来咱们就来编写Table View的代码。
首先是Cell:
新建一个swift文件,命名为TableViewCell.swift
定义一个类:TableViewCell,声明为UITableViewCell的子类,而且用Interface Builder链接以前在storyboard里面建立的Cell中的Label。加入一个字符串常量,和以前输入的identifier同样。最终看起来应该是这样(做者使用了本身的类前缀LF):
在storyboard中更改Cell的类型:
这样,Cell的就定义好了。这能够被很容易地改成更复杂的情形。
以后是Table View自己的代码。UITableView采用了Data Source - Update的模式。这种模式的实现须要一个Data Source。
在做者的实现中,须要先有一个ViewModel来封装数据。为此,创建一个swift文件,名为MainViewModel.swift,并加入如下代码:
struct MainViewModel { var resultCount: Int! var results: [String]! static func isValidSearchString(text text: String) -> Bool { return text.characters.count > 4 } static func produceSearchResult(text text: String) -> [String] { return (1...text.characters.count).map { i in return "somebody \(i)" } } }
这里定义了封装数据的格式,并提供了两个辅助函数。
在这个例子中这两个函数至关简单,可是随着代码变得复杂,把操做聚合起来是颇有利的。
以后新建一个swift文件,命名为ResultViewController.swift,加入如下代码:
import UIKit class LFResultViewController: NSObject, UITableViewDataSource { var viewModel = MainViewModel() @objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModel.resultCount } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cellRetired = tableView.dequeueReusableCellWithIdentifier(LFTableViewCell.identifier) as? LFTableViewCell if cellRetired == nil { cellRetired = LFTableViewCell(style: .Default, reuseIdentifier: LFTableViewCell.identifier) cellRetired?.outputLabel = UILabel() } cellRetired?.outputLabel.text = self.viewModel.results[indexPath.row] return cellRetired! } }
这里实现了Table View更新时须要的数据的方法。具体的关于UITableViewDataSource
协议的内容能够查看Apple的官方文档.
这个模式看起来比较复杂,不像其余控件那样直截了当的就是赋值。但这种模式对响应式编程和MVVM模式特别有利,做者在将来可能会为整个UIKit开发这样的接口来避免使用keypath。
这里先贴出ViewController.swift中其余部分代码:
import UIKit import ReactiveCocoa class ViewController: UIViewController { @IBOutlet weak var outputTableView: UITableView! @IBOutlet weak var searchTextInput: UITextField! var resultViewController = LFResultViewController() override func viewDidLoad() { super.viewDidLoad() self.resultViewController.viewModel.resultCount = 0 self.resultViewController.viewModel.results = [] self.outputTableView.dataSource = self.resultViewController self.outputTableView.reloadData() } }
最关键的部分开始了!
RayWenderlich.com上的教程中使用了Signal,可是在swift中,ReactiveCocoa的开发者并无提供泛型的Signal,而使用AnyObject来和Objective C兼容。但这个严重损伤了代码的可读性,也让bug有了遁身之处。所以在这里,咱们将使用SignalProducer,这样可使用swift的类型检查来杜绝bug。
在viewDidLoad
中加入如下代码:
let searchText = searchTextInput.rac_textSignal() .toSignalProducer() .map {text in text as! String}
map函数把一个集合类型(Array例如)中的元素逐一操做,并返回新的元素构成的集合类型。用Array举例能够清楚地解释这一切。(不恰当地说,你能够把SignalProducer当成一个Array,startWithNext就至关于for-in)
[1, 2, 3, 4].map {$0 * 2} //[2, 4, 6, 8]
这里把一个RACSignal转化成了一个SignalProducer,而且这个是有类型检查的。为了更好地使用类型检查,咱们把本来的SignalProducer<AnyObject, NSError>
转化成了一个SignalProducer<String, NSError>
。在这里,做者想说,Xcode编辑器的类型检查能够很好的帮助咱们避免一些问题。尤为是在接下来构造Monad结构时,经常能够三指点按(即LookUp手势)来查看构造出来的对象的类型。这能够帮助理清楚Monad每一步的流程。
接下来,咱们有了用来搜索的文本,咱们要先执行一些过滤。在viewDidLoad
中加入如下代码:
searchText.filter(MainViewModel.isValidSearchString)
仍是用Array举例说明:
[1, 2, 3, 4].filter {$0 % 2 == 0} //[2, 4]
这样,咱们就用以前定义的过滤函数来检查字符串是不是合法的搜索字符串。这个方法输出的仍是一个SignalProducer<String, NSError>
。
咱们还须要作一点过滤,也就是说,只有当用户输入在500毫秒内没有变化时,更新才会被触发。为此咱们须要throttle函数。在以前那句话下面加上
.throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler)
注意前面的点。咱们事实上是在调用上一步结果的一个方法。这就是Monad的特色:流畅接口。每一步都会构造一个对象,下一步调用它的方法。Xcode彷佛并不喜欢Monad结构,缩进作得不好。Xcode 8和Xcode Extension或许能够解决这个问题,可是如今还得手工格式化。还有swift编译器在处理链式调用时会出现一些问题,最多见的是报错:Expression too complex to be resolved in reasonable time. 这种时候只须要在链式调用的每一个点以前换一行就能够了。这也是推荐的用法。
接下来是传统方法最费力一部分了:异步请求。(Accept the fact that we are living in a asynchronous world)所幸ReactiveCocoa提供了一个良好的方法来解决这个问题:把它们包装成SignalProducer。可是咱们在这里遇到一个问题:若是用map而且返回一个SignalProducer,咱们将会在下一步获得一个SignalProducer<SignalProducer<([String], Int), NoError>, NoError>。这显然不是咱们想要的。这里就要介绍flatMap函数了。先看Array的举例:
[1, 2, 3, 4].map {[$0]} //[[1], [2], [3], [4]] [1, 2, 3, 4].flatMap {[$0]} //[1, 2, 3, 4]
也就是说,flatMap方法会自动“剥掉一层”。加上代码:
.flatMap(.Latest) { (text: String) in return SignalProducer { (o: Observer<([String], Int), NoError>, c: CompositeDisposable) in let rst = MainViewModel.produceSearchResult(text: text) let cnt = rst.count o.sendNext((rst, cnt)) o.sendCompleted() } }
经过这个flatMap,咱们把原来的SignalProducer<String, NSError>转化成了SignalProducer<([String], Int), NoError>。(注意使用NoError类型须要包含Result框架)
如今,咱们有了“搜索结果”,能够去显示了。
加上代码:
.observeOn(UIScheduler()) .startWithNext { [weak self] (x: ([String], Int)) in if let strong = self { strong.resultViewController.viewModel.resultCount = x.1 strong.resultViewController.viewModel.results = x.0 strong.outputTableView.reloadData() } }
这里有两个要说明的地方:一是在iOS上只有主线程能够更新UI,所以咱们须要借助UIScheduler来把工做转移到主线程。还有为了不循环引用,咱们须要声明一个[weak self]
来告诉编译器咱们不但愿闭包持有对self的引用。详细说明
最终,viewDidLoad函数应该看起来像这样:
override func viewDidLoad() { super.viewDidLoad() self.resultViewController.viewModel.resultCount = 0 self.resultViewController.viewModel.results = [] self.outputTableView.dataSource = self.resultViewController self.outputTableView.reloadData() let searchText = searchTextInput.rac_textSignal() .toSignalProducer() .map {text in text as! String} searchText.filter(MainViewModel.isValidSearchString) .throttle(0.5, onScheduler: QueueScheduler.mainQueueScheduler) .flatMap(.Latest) { (text: String) in return SignalProducer { (o: Observer<([String], Int), NoError>, c: CompositeDisposable) in let rst = MainViewModel.produceSearchResult(text: text) let cnt = rst.count o.sendNext((rst, cnt)) o.sendCompleted() } } .observeOn(UIScheduler()) .startWithNext { [weak self] (x: ([String], Int)) in if let strong = self { strong.resultViewController.viewModel.resultCount = x.1 strong.resultViewController.viewModel.results = x.0 strong.outputTableView.reloadData() } } }
编译运行,程序如预期执行。
ReactiveCocoa表明了一种全新的方式。它的核心就在于:“高聚合 低耦合” 同时具备强大的异步处理能力。Forget dispatch_async, let's startWithNext.