在寫我的週末習作時, 為了抓取bitmap上所有的pixels時, 碰到這問題, 找了整個晚上才找到答案

首先, 要抓取bitmap上所有的pixel必須要使用到CoreGraphics裡的CGBitmapContextGetData, 想當然爾, 這是老舊的API, C API, 那用Swift可以去取用嗎? 參照Interacting with C APIs 這篇裡面有相關的Swift與C之間的對照

回到取pixels這目的, 我希望取到的值是一個整數陣列, 精確來說是[UInt32]的陣列, 但CGBitmapContextGetData回傳的是什麼呢? 查文件可知是UnsafeMutablePointer<Void> ,依照上面的文件來說, 對照到C則是"void *“, 文件裡面有提到:

When a function is declared as taking an UnsafeMutablePointer<Void> argument, it can accept the same operands as UnsafeMutablePointer<Type> for any type Type.

意思就是, 它也可以是整數陣列指標, 但, 怎麼轉回去呢? 這就是讓我整個晚上超崩潰的, 幹, 這文件沒寫!

搞半天, 才終於讓我找到答案:

let data = CGBitmapContextGetData(context)
    
let intData = UnsafeMutablePointer<UInt32>(data)
let intArray = Array(UnsafeBufferPointer(start: intData, count: bitmapByteCount))

原來必須先把它轉型成UnsafeMutablePointer<UInt32>, 然後再用UnsafeBufferPointer轉存到實際的陣列中(因為UnsafeBufferPointer是SequenceType, 才可以轉成Array), 當然也要給它陣列大小

但實際上這樣做對不對, 安不安全…還不知道, 找到答案寫下來先

主要是看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去, 挺有趣的(雖然也不是很好懂)