主要是看Medium iOS版的搜尋畫面, 覺得他在UITextField裡顯示載入動畫還蠻不錯的, 不會有礙整個畫面的觀瞻, 所以就想做成像這樣:

其實這也不難, 靠的是UITextField的rightView(有rightView和leftView可用), 只要把rightView指定到一個UIActivityIndicatorView, 把rightViewMode設成.Always, 然後呼叫startAnimation就可以了:

https://github.com/julianshen/RAC-GitHubUserSearch/blob/aa3754787f2eec9e779493dffc65a65aa918c50a/GithubUserSearch/ViewController.swift#L39-L41

另外在loading結束後把rightViewMode設成.Never

https://github.com/julianshen/RAC-GitHubUserSearch/blob/master/GithubUserSearch/ViewController.swift#L52-L54

那怎判斷loading開始了呢? 那就要借重另一個東西, 在呼叫GitHubAPI前呼叫observe:

https://github.com/julianshen/RAC-GitHubUserSearch/blob/aa3754787f2eec9e779493dffc65a65aa918c50a/GithubUserSearch/ViewController.swift#L35-L44

今天的東西要分兩篇寫

之前下載圖的地方: 

https://github.com/julianshen/RAC-GitHubUserSearch/blob/9183f978ee782fbf8e118c79a9557f294e8aa87b/GithubUserSearch/ViewController.swift#L65-L84 

有個缺點, 就是當UITableView已經被更新或是滑到下一個頁面時, 圖片下載並沒有被中斷, 但cell又是被回收使用的, 導致前一張下載完後還是會先顯示到UIImageView上去, 這樣會造成圖片跳來跳去的

最直覺的想法是當圖還沒下載完之前這個cell已經要被重複利用到別地方前就終止下載, 但在RAC裡面, Signal是沒辦法被中途停止的, 但可以被unsubcribe, 因此改寫成:

https://github.com/julianshen/RAC-GitHubUserSearch/blob/aa3754787f2eec9e779493dffc65a65aa918c50a/GithubUserSearch/ViewController.swift#L81-L96

這邊用到一個東西就是Disposable, 利用這東西的dispose()方法, 就可以unsubscribe 還沒做完的Signal了

過了幾天(尤其在這兩天)的摸索後, 終於把第一支App給兜出來了, 這只是個練習, 所以做的事情很單純 - 搜尋GitHub上的User

Source code 在: RAC-GitHubUserSearch

範例畫面: Imgur

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

對FRP跟Swift還沒很熟, 直接玩ReactiveCocoa 有點苦手, 不過, 還挺好玩的

狀況設計: 畫面上有一個輸入框, 和一個文字標籤, 在輸入框輸入文字後, 標籤要同步顯示一樣的文字, 文字框輸入不滿三個字母時背景要顯示紅色,像這樣:

所以會有一個標籤(myLabel)和一個輸入框(myInput):

    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var myInput: UITextField!

要做到這樣的功能, 不只有一種作法, 像是也可以用KVO去observe一個變數由那個來改變myLabel的值,但KVO的缺點是, 被observe的那個必須是dynamic的, 所以沒辦法直接observer myInput.text

但用ReactiveCocoa來做的話就簡單的有趣, 首先如果要達到第一個需求, 只需要

    var text_signal = myInput.rac_textSignal()
    text_signal.setKeyPath("text", onObject: myLabel)

這還真是么壽簡單, 只是把myInput.text跟myLabel.text抓條線連一起就好了!

那第二項呢?


text_signal.map({
  (textObj:AnyObject!)->AnyObject in
    var text = textObj as! String
    return count(text) < 3 ? UIColor.redColor():UIColor.whiteColor()
}).setKeyPath("backgroundColor", onObject: myInput)

這也是簡單的不行, 把輸入的Signal用map轉換成UIColor, 而轉換的條件就是以text的長度來計算, 最後連到myInput.backgroundColor去, 挺有趣的(雖然也不是很好懂)

(通車的路上寫blog最討厭碰到網路出狀況呀!) KVO (Key Value Observing) 是用來監控一個變數是否有改變的技巧, 在做UI的data binding來說是蠻有用的, 舉個例子, 如果有個Label是一直顯示網路蒐集過來的讀數會一直變化, 一般的作法是收到這讀數再直接去操作設定這個label, 缺點是會把這類的資料邏輯跟UI邏輯綁死, 而且如果是有多個以上要跟這資料連動, 較不易擴充 (還沒完全醒, 例子舉得很爛), KVO提供一個比較好的方式利用監控數值變化來處理這件事

網路上找到一篇文章, 或許這篇解釋的比我好: 了解 Objective-C 上的 KVO(Key-Value Observing) 機制

不過這篇主要是針對Objective C的, 觀察者利用實作observerValueForKeyPath來得到狀態, 而被觀察者利用了addObserver加入觀察者

當數值變動時, 必須要透過willChangeValueForKey 和 didChangeValueForKey 來通知觀察者, 這點算比較麻煩, Swift則不需要自己去呼叫, Swift在語言本身來說就已經設計了監看變數變化的特性, 最簡單的方式是透過willSet和didSet, 例如:

  var myVar:Int {
     willSet {   
       ....
     }
     didSet {
       ....
     }     
  }

不過這樣其實還沒達到KVO的效果, 只算是一個hook來處理數值變化時要做的事, 在Swift中也是透過addObserver和observerValueForKeyPath, 唯一不同的是不需要willChangeValueForKey 和 didChangeValueForKey, 基本上只要把變數宣告成dynamic就好

dynamic var myVar: String = "ssss"

這邊很重要的是一定要宣告成dynamic, 不然會沒效果喔!

(剛好到站, 寫完下車)