過了幾天(尤其在這兩天)的摸索後, 終於把第一支App給兜出來了, 這只是個練習, 所以做的事情很單純 - 搜尋GitHub上的User
Source code 在: RAC-GitHubUserSearch
範例畫面:
swift還不太熟, 所以讀人家的code和寫code還不是那麼的流暢, 不過最大的問題還是在硬是想要用Swift + RAC 3.0, RAC 3.0現在雖然已經進入beta了, 但很多東西還不太穩定, 還在改, 自然不用說文件了, swift相關的官方文件幾乎是0, 這時候Google, source codes, 還有github上的每一則issue就是最好的朋友了
熟悉funtional language的人可能對reative的觀念會比較容易懂, 目前看過寫的比較不錯的文章是這篇:
The introduction to Reactive Programming you’ve been missing
反正不就是把一切當stream串來串去的不就是了嗎?(其實會暈), bacon.js這幾張圖也有點幫助理解一些工具
在ReactiveCocoa方面, Colin Eberhardt寫了好幾篇文章還算蠻不錯的:
http://blog.scottlogic.com/2015/04/24/first-look-reactive-cocoa-3.html
http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
不過針對RAC3的文章尚不完整加上RAC3 Swift的部份也還一直在改, 其實很多東西看code可能會比較清楚, 為了研究這個, 看了幾十篇文章, ObjectivC的相關的未必適用於Swift, 有些API在Swift上是改了名字改了用法, 甚至有些其實還沒有, 整理上蠻頭痛的
對於MVVM的部份, 我其實也還沒很認真搞懂, 還是有點似是而非
針對我的code裡面幾個地方來說明一下好了
DynamicProperty
DyamicProperty的觀念的部份可以看這篇: ReactiveCocoa (RAC3.0) by Swift
啥?日文的?沒關係, 看到關鍵字KVO就大概猜是跟KVO類似的東西, 我的code裡面ViewController.viewDidLoad的第一段:
viewModel.prop_keyword <~ keywordInput.rac_textSignal().toSignalProducer() |> catch {
error in
return SignalProducer<AnyObject?, NoError>(value:"")
}
這一段就是把UITextField打的字自動設定到viewModel.searchKeyword內, 這邊故意把searchKeyword宣告成private, 是為了要實驗這樣signal也還可以透過DynamicProperty設定這邊的值
這邊比較難懂的地方是"<~“, ”|>“, 這兩個都不是Swift標準的運算符, 而是ReactiveCocoa特有的, ”<~“尤其特別, 這跟我們慣用的左向右的運算符不太一樣, 這要由右向左看, 就是把左邊的signal producer的結果串到右邊的DynamicProperty, 而”|>“呢?對於reactive來說, 都是stream的操作, 所以Signal也是可以串接起來的, 透過”|>“, 那這邊為何要多一個”|> catch"呢? 因為DynamicProperty接的是SignalProducer<U, NoError>但textSignal的Error則非NoError, 需要靠catch來做error handling
DynamicProperty跟一般的KVO不同的地方就是它可以是雙向的, 因此可以用:
viewModel.prop_keyword.producer
|> filter {
return count($0 as! String) >= 3
}
這邊就是把prop_keyword這個DynamicProperty當SignalProducer來用, 串到filter去
另外這一整段:
let filteredKeywordSignalProducer = viewModel.prop_keyword.producer
|> filter {
return count($0 as! String) >= 3
}
let throttledKeywordSignal = filteredKeywordSignalProducer |> throttle(1, onScheduler: QueueScheduler())
throttledKeywordSignal |> map {
value in
return GitHubClient.searchUser(value as! String)
}
|> flatten(FlattenStrategy.Concat)
|> start {
value in
self.viewModel.users = value
}
這邊的目的是, 當UITextField打超過3個(含)字母以上時, 要透過GitHub API去搜尋使用者並改變viewModel users裡的值
其時map + flattern這邊本來想用flatMap來寫的, 不過flatMap似乎有點問題, 參考:
https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1966#issuecomment-99536117
以及這個commit: https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1988
目前Signal和SignalProducer各有一個flatMap, compile會有ambiguous的情形, 看來這個commit會把SignalProducer那個拿掉, 但還是可以用map + flatten改寫
另外throtte的用意是為了避免太頻繁的送出API, 限制request的間隔在一秒內
接下來是signalForImage的部份:
func signalForImage(url: NSURL) -> SignalProducer {
return SignalProducer {
observer, disposable in
let imgData = NSData(contentsOfURL: url)
sendNext(observer, UIImage(data: imgData!)!)
sendCompleted(observer)
return
}
}
這部份是把載入網路上的圖這功能包裝成一個signal producer, 目的是為了下面這段:
signalForImage(url!)
|> startOn(QueueScheduler())
|> observeOn(UIScheduler())
|> start
{
value in
cell.imageView?.image = value
return
}
startOn(QueueScheduler())的目的就是要讓載入圖片這件事不要在主線程(main thead)發生, 而是把它丟到GCD queue, 最後UI thread接到通之後再把它顯示出來
好了, 先到這邊, 整個程式看起來很簡單, 但很多觀念還花了不少時間呀 orz