「集合知プログラミング」を読んでる
「集合知プログラミング」という本を先週から読み始めた。この本は機械学習をテーマとしていて、現実にありそうな問題(例えば、映画の評点から似ているユーザーを推薦するとか、数ある旅行プランの組み合わせから最適なプランを選択するとか)を題材にさまざまなアルゴリズムをチュートリアル形式で学んでいける。登場するサンプルコードはすべてPythonで書かれているため、まずこの本を読む前に軽くPythonについて勉強した。機械学習の本というと、むずかしい数式がたくさんでてきて近寄りがたいイメージがあるのだけど、この本についてはほとんど数式は出てこないので、カジュアルに読み進められる。
まだ半分も進んでないけど、その中で一番おもしろかったのが最適化アルゴリズムの話だった。ある最適な値を求めたいとき、「となりあう値と比較して良い方を選択する」というのを繰り返していくとどこか最適な値で落ち着くはずというアルゴリズム(ヒルクライム)があるのだけど、これだと局所最適に陥ってしまうということを最近勉強した。つまり、全体を見渡すともっと最適な値があるのだけど、近くの値とだけ比較しているとそれを見逃してしまうということだ。また、別のアルゴリズム(模擬アニーリング)は、試行回数が少ないうちは悪い結果を受け入れ、回数を経るにつれてその悪い結果を受け入れ難くしていくことで局所最適を回避する。
これはいろんなところで当てはまりそうな考え方だなと思った。見える範囲、理解できる範囲だけで最適な選択をとろうとするとより適切な解を見落としてしまう。若いうちは結果が悪かろうともそれを受け止めることで局所最適を回避し全体最適に近づくことができるのかもしれない。
この本を読もうと思った理由としては、いろんな領域と機械学習を組み合わせるとなんか面白いものが作れそうな気がしたから。これまで自分が作ってきたソフトウェアの中で自分自身気に入っているものの多くは別の領域のアイデアを持ち込むところから生まれている。だから、組み合わせの可能性が大きい領域を何か新しく学びたいと思ったときに機械学習というものが浮かんでてきてこの本から取り組んでみることにした。今は「iOS x 機械学習」みたいな掛け合わせで何か面白いものが作れないか考えている。
- 作者: Toby Segaran,當山仁健,鴨澤眞夫
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/07/25
- メディア: 大型本
- 購入: 91人 クリック: 2,220回
- この商品を含むブログ (277件) を見る
ストリームを利用したローパスフィルタの実装
このスクリーンショットに映された2つの線は共にiPhoneの加速度センサーの値を表しており、下の緑が加工していない生データ、上の青い線がローパスフィルタという仕組みで揺れを除去したデータだ。
以前の記事でSwiftを使ったストリームの実装をしてみたのだけど、その使いどころを考えてみたところセンサーデータの加工にストリームという概念が適しているのではないかと思いついた。センサーから送られてくるデータは連続的で、その加工には複雑な計算を要するためだ。
そこで、加速度センサーをグラフに表示する簡単なアプリを作ってみて、生データとストリームを使って加工したデータを視覚的に表現してみることにした。その結果が上のスクリーンショットとなる。今回はローパスフィルタと呼ばれる手法を用いて生データを加工した。そちらの方面にはまるっきり分からないのだけど、以下のようなとてもシンプルなアルゴリズムでデータを加工できるとのことだったので利用した。
今回の加工したデータ = 前回の加工したデータ * 0.9 + 今回の生データ * 0.1
このローパスフィルタを以前開発したストリームライブラリで実装してみる。
var x: [CGFloat] = [] var filteredX: [CGFloat] = [] let xStream = Stream<CGFloat>()
まず、生データと加工したデータをグラフに描画するための配列と生データを扱うストリームを用意する。加速度センサーから値を取得する度にこのストリームに値を出力していく。
override func viewDidLoad() { // ... motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue(), withHandler: accelerometerHandler) } private func accelerometerHandler(data: CMAccelerometerData!, error: NSError!) { xStream.publish(CGFloat(data.acceleration.x)) }
ストリームに渡された生データをグラフに描画するための配列に入れるため、値が出力されたときに実行される関数を登録しておく。これで生データが出力されたときはいつでもこの関数が実行される。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } }
続いて、上のストリームに出力された生データを加工して出力する別のストリームを作成する。これはscan
関数を利用することで簡単に実現できる。scan
関数は「前回出力された値と今回出力された値を使って、新たな値を出力するストリーム」を簡単に作成できる。なので、上で示したローパスフィルタのアルゴリズムを以下のように実装することができる。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } let filteredStream: Stream<CGFloat> = xStream.scan(0) { previousMessage, message in return previousMessage * 0.9 + message * 0.1 } }
最後に、加工した値の出力を見張ってグラフ描画用の配列に追加するための関数を登録しておく。
override func viewDidLoad() { // ... xStream.subscribe { [unowned self] message in self.x.append(message) } let filteredStream: Stream<CGFloat> = xStream.scan(0) { previousMessage, message in return previousMessage * 0.9 + message * 0.1 }.subscribe { [unowned self] message in self.filteredX.append(message) // ... } }
このように、ストリームの性質やストリームを扱う様々な関数を利用すると、簡単にセンサーデータを扱うプログラムを実装することができた。アプリのソースコードはgithubにアップしてあるので、参考にしてほしい。
マシなiOSアプリのフォームを実装・デザインする
普段iOSのフロント寄りの実装やデザインについて手が着けられていなかったけど、Xcode6の新機能のおかげでそっちも興味がでてきたので、ログインフォームを想定してiOSアプリのフォームの設計について本気出して考えてみた。
最もシンプルなフォーム
- メールアドレス用の
UITextField
(以下emailField
)、パスワード用のUITextField
(以下passwordField
)、そしてログインボタン用のUIButton
(以下loginButton
)の3つをStoryboardで配置した。 emailField
はKeyboard TypeをE-mail Addressに、Return KeyをNextに設定した。passwordField
はSecure Text EntryのチェックをオンにしReturn KeyをGoに設定した。
問題点
emailField
でReturn Keyを押してもpasswordField
が選択されないし、passwordField
でReturn Keyを押してもsubmitされない。- コントロール部品以外をタップしたとき、キーボードが閉じない。端末サイズが小さい場合、キーボードによって他のコントロールや表示すべきViewが隠れたままになる可能性がある。
- 追加した3つのViewが指の大きさに対して小さい。ユーザーは正確にタップするために注意を向ける必要があり、間違ったViewをタップしてしまう可能性がある。
改善1: Return Keyで適切なアクションを起こす
// ViewController.swift @IBOutlet var emailField: UITextField? @IBOutlet var passwordField: UITextField? @IBOutlet var loginButton: UIButton? @IBAction func login() { println("Login") } // MARK: - UITextFieldDelegate func textFieldShouldReturn(textField: UITextField) -> Bool { if (textField == emailField) { passwordField?.becomeFirstResponder() } else { login() } return true }
login()
はloginButton
が押された場合、またはpasswordField
でReturn Keyが押された場合に実行される。今後、このメソッドにログイン処理を実装していく予定。emailField
とpasswordField
のdelegate
をこのViewControllerに設定しtextFieldShouldReturn(textField:)
を実装することで、2つのUITextFieldでReturn Keyが押されたときの処理を実装できる。becomeFirstResponder()
はレシーバーのViewを最初に応答するオブジェクトとして設定する。キーボードはこのFirst Responderに合わせてキーボードタイプや入力先を替える。
改善2: キーボードを閉じる
@IBAction func login() { resignFirstResponderAtControls() println("Login") } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { resignFirstResponderAtControls() } private func resignFirstResponderAtControls() { emailField?.resignFirstResponder() passwordField?.resignFirstResponder() }
- 非公開メソッドとして
resignFirstResponderAtControls()
を定義した。これによって2つのUITextFieldの選択状態を外しキーボードを閉じることができる。resignFirstResponder()
メソッドはレシーバーのViewをFirst Responderでなくす。これによってキーボードが閉じる。 - これを
login()
とtouchesBegan(touches:withEvent:)
で呼び出す。 UIViewController
はUIResponder
を継承しておりself.view
のイベントハンドリングを扱うことができる。そのため、touchesBegan(touches:withEvent:)
でresignFirstResponderAtControls()
を呼ぶことで、追加した3つのView以外を選択されたときにキーボードを閉じることができる。
改善3: タップしやすくする
「ヒューマンユーザーインターフェイスガイドライン」(以下HIG)にはこのような指針が載っている。
アプリケーション内のタップ可能な要素には、約44x44ポイントのターゲット領域を割り当てる。
これに従って「44x44ポイント以上」にサイズを変更する。
まず、UITextField
は高さが30ポイントに固定されているため、高さ44ポイントのViewの上にUITextField
を乗せてボーダーを非表示にし、その親ViewがタップされたらUITextField
がFirst Responderになるようにする。実装としては、UITextField
を含む高さ44ポイントのUIView
のサブクラスを用意する。
// TextFieldContainer.swift @IBDesignable class TextFieldContainer: UIView { @IBInspectable var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { subviews.first?.becomeFirstResponder() } }
Xcode 6からの新機能であるLive Viewsを利用し、カスタムViewも可能な限りStoryboard上でそのプロパティを変更できるようにする。
@IBDesignable
によってカスタムViewをStoryboard上でレンダリングして、その見た目をStoryboardからも確認できるようになる。@IBInspectable
によって下のスクリーンショットのようにカスタムViewのborderWidth
というプロパティをStoryboardから変更できるようになる。
次に、UIButton
もHIGの方針に従って修正する。UIButton
はサイズを自由に変更できるので、とりあえず44x44ポイントに変更した。ボタンの大きさは変更したものの、ボタンの「Login」というテキストはまだ小さいため、ユーザーの目からはサイズが大きくなったようには見えていない。そこで、ボタンにもボーダーをつけてみる。
// BorderedButton.swift @IBDesignable class BorderedButton: UIButton { @IBInspectable var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } } }
TextFieldContainer
と同じようにstoryboardから枠線の幅を変えられるようにした。
マシなフォーム
以上の変更を行った結果このようになった。
間違いなくタップはしやすくなった。
問題点
- フラットデザインに則っていない。標準のアプリや人気の高いアプリはiOS 7から導入されたフラットデザインに沿ってデザインされており、同様なインターフェイスをもたなければユーザーは慣れ親しんだ動作で直感的にアプリを操作できなくなってしまう。
改善4: フラットデザインに従う
HIGではフラットデザインの基本的な設計方針として以下の3つを挙げている。
- 控えめであること
- 明瞭であること
- 奥行きを与えること
具体的な作業として
- 画面全体を使う
- 枠線を使わない
- 余白を十分にとる
を意識してStoryboardを編集した。Auto Layoutで各Viewの余白を固定したり、枠線の太さを0ポイントにした。その結果、以下のようになった。
フラットデザインに対するよくある批判として「ボタンがどこにあるのか認識しにくい」というものがある。"Email"や"Login"といった文字がある部分にしかViewがないように見えてしまうため、Viewの領域を表す枠線や背景色を控えめに加えた方がもっとよくなると考えた。そこで、2つのUITextField
の領域を控えめに表すため、領域の下辺だけ枠線を表示してみる。
// TextFieldContainer.swift @IBDesignable class TextFieldContainer: UIView { private var width: CGFloat { return CGRectGetWidth(frame) } private var height: CGFloat { return CGRectGetHeight(frame) } private let borderBottom: CALayer = CALayer() @IBInspectable var borderColor: UIColor = UIColor.blackColor() { didSet { setupBorderBottom() } } @IBInspectable var borderBottomWidth: CGFloat = 0 { didSet { setupBorderBottom() } } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { subviews.first?.becomeFirstResponder() } private func setupBorderBottom() { borderBottom.removeFromSuperlayer() borderBottom.frame = CGRectMake(0, height - borderBottomWidth, width, borderBottomWidth) borderBottom.backgroundColor = borderColor.CGColor layer.addSublayer(borderBottom) } }
- 以前の実装にあった
borderWidth
を削除し、下辺の枠線の太さを表すborderBottomWidth
と枠線の色を表すborderColor
を追加した。これらのプロパティがXcodeから変更されるたびにsetupBorderBottom()
が呼び出されてborderが追加される。 - 下辺だけの枠線は枠線の幅を高さとする
CALayer
として実装した。 - ショートカットのため
width
とheight
というcomputed propertyを用意した。
そして、Storyboardから枠線の色と幅を設定し余白を調整すると以下のようになった。
まとめ
最初と最後を比べると少しはマシなフォームになったと思う。改善したポイントをまとめると以下のようになる。
- Return Keyで適切なアクションを起こす
- キーボードを閉じる
- タップしやすくする
- フラットデザインに従う
現実の開発では、アプリごとのテーマに合わせた色やタイポグラフィを使うことになるだろうし、フォームのエラーメッセージの扱いについても触れられていない。残された課題については、経験を積む中で考えていくことにしたい。
最後に上で載せたコードを含んだプロジェクトをGitHubに公開したので参考にしてほしい。
Swiftでストリームを扱うライブラリを書いた
FRPの記事をいくつか見てあまり理解できなかったので、Swiftでストリームを扱うライブラリを書いてみた。結論から言うと、まだストリームについて深く理解できていない感じがするので「FRPとは何か」「ストリームとは何か」といった話はしない。そういう話は他のエントリーを読んでほしいと思う。
demo
let stream = Stream<String>() let counterStream: Stream<Int> = stream.map({ message in return countElements(message) }).scan(0, { previousMessage, message in return previousMessage + message }).subscribe({ message in println(message) }) stream.publish("Hello, ") //=> 7 stream.publish("wor") //=> 10 stream.publish("ld!") //=> 13
機能
- ストリームに
subscribe
で関数を渡すと、publish
されたときにその関数がpublish
の引数が渡されて実行される。subscribe
は失敗時と完了時に実行する関数を指定することもできる。ストリームに失敗を通知するのはpublish
の代わりにfail
、完了を通知するのはcomplete
。 - その他FRPのライブラリで実装されている次のような基本的なストリーム操作を実装した:
map
,filter
,scan
,flatMap
,throttle
,debounce
,buffer
,merge
結果
SwiftでNSDateを簡単に扱うライブラリを書いた
Swiftの実験的なプロジェクトとしてActiveSupportの拡張っぽく直感的に時間を扱うライブラリ"Timepiece"というものを書いた。
demo
let today = NSDate.today() let tomorrow = NSDate.tomorrow() let dayAfterTomorrow = tomorrow + 1.day let dayBeforeYesterday = 2.days.ago let birthday = NSDate.date(year: 1987, month: 6, day: 2)
機能
1.day.ago
(1日前)、4.years.later
(4年後)というようにInt
型を拡張し、数.単位.前/後
という書き方でNSDate
オブジェクトを初期化できる。単位は単数形、複数形どちらも使える。NSDate() + 1.minute
(1分後)、NSDate() - 3.hours
(3時間前)というように、NSDate
オブジェクトから数.単位
を加算・減算できる。NSDate.date(year:month:day:hour:minute:second)
,NSDate.today()
,NSDate.yesterday()
,NSDate.tomorrow()
というように、NSDate
オブジェクトをより直感的に初期化できる。
今後
- 既存の
NSDate
オブジェクトの時間だけ変更したいとか、その日の0:00を取得したいという場合に対応したい。ActiveSupportとかmoment.jsのbeginning_of_day
,endOf("day")
のようなやつ。 - 範囲オブジェクトのようなものを生成できるようにしたい。例えば、今日から1週間分の
NSDate
オブジェクトの範囲オブジェクトを作って繰り返し処理させられたら便利そう。
悩み
こういうライブラリでよくあるのがパース処理とフォーマット処理なんだけど、このライブラリでサポートすべきかどうか悩む。というのも、この手の処理はNSDateFormatter
オブジェクトを使うと思うのだけど、便利なAPIのせいで無意識にムダなNSDateFormatter
オブジェクトを何度も生成してしまう可能性がありそうだなと懸念している。
プルリクチャンス
- こういう機能がほしいという希望があればissuesでどんどん要望を伝えてほしいです。
- もちろんPull requestも歓迎です。コード量は多くないので、ちょっと読めば何をしているか思います。テストはちゃんと書いてます。
Alamofire/**/*Tests.swiftを読んだ
前回Alamofireの実装を読んだので、ついでにテストコードも読んでみた。
$ tree -P "*Tests.swift" -I .git . ├── Source └── Tests ├── DownloadTests.swift ├── ParameterEncodingTests.swift ├── RequestTests.swift ├── ResponseTests.swift └── UploadTests.swift
とりあえずRequestTests.swift
から読んでみる。
RequestTests.swift
L:26
extension Alamofire { struct RequestTests { class RequestInitializationTestCase: XCTestCase { // ... } class RequestResponseTestCase: XCTestCase { // ... } } }
- このプロジェクトはビルドターゲットをもたないため、ターゲットごとに名前空間が作られるわけではない。そのため、
Alamofire
という構造体の中にテストクラスをネストさせることで実装コードを参照できるようにしている。 - テストケース、この場合は
init
とかresponse
といったメソッドの単位でテストケースのクラスを作っているようだ。
L:49
class RequestResponseTestCase: XCTestCase { func testRequestResponse() { let URL = "http://httpbin.org/get" let serializer = Alamofire.Request.stringResponseSerializer(encoding: NSUTF8StringEncoding) let expectation = expectationWithDescription("\(URL)") Alamofire.request(.GET, URL, parameters: ["foo": "bar"]) .response(serializer: serializer){ (request, response, string, error) in expectation.fulfill() XCTAssertNotNil(request, "request should not be nil") XCTAssertNotNil(response, "response should not be nil") XCTAssertNotNil(string, "string should not be nil") XCTAssertNil(error, "error should be nil") } waitForExpectationWithTimeout(10){ error in XCTAssertNil(error, "\(error)") } } }
expectationWithDescription
とwaitForExpectationWithTimeout
はXcode 6からXCTestに追加された非同期テスト用のAPI。- まず、
expectationWithDescription
メソッドでXCTestExpectation
オブジェクトを生成する。 waitForExpectationWithTimeout
は指定された秒数、上で生成されたXCTestExpectation
オブジェクトのfulfill
メソッドが呼ばれるのを待つ。呼ばれれば成功、呼ばれずに指定された秒数が経過すると失敗となり引数に渡されたクロージャを実行する。
他のテストコードも読んでみたけど、上と同じようなコードがあり特に読む必要はなさそうだった。
Alamofireを読んだ
Alamofireとは
- https://github.com/Alamofire/Alamofire
- Swiftで書かれたHTTP通信ライブラリ。
- AFNetworkingの作者であるmatttさんの新作。
- AFNetworkingをリプレースするものではなく、AFNetworkingはSwiftでも安定して動くのでそのまま使えるとのこと。(参考: http://nshipster.com/alamofire/の最後の方)
- ファイルは
Alamofire.swift
だけで1000行に満たない。
使い方
Alamofire.request(.GET, "http://httpbin.org/get") .responseJSON { (request, response, JSON, error) in println(JSON) } Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"]) .authenticate(HTTPBasic: user, password: password) .responseJSON { (request, response, JSON, error) in println(JSON) } .responseString { (request, response, string, error) in println(string) }
※読んだコードのコミット番号は76266c95564912f228e76a1868e50b6a33f104e7
である。
Alamofire.swift
tl;dr
Manager
オブジェクトが通信を行い、通信完了時のdelegateオブジェクトを管理する。- 初期化時に
NSURLSession
オブジェクトやdelegateオブジェクトをプロパティとして保持する。
- 初期化時に
request
メソッドは以下のことをする。response
メソッドは以下のことをする。- 通信が完了するとdelegateメソッドは以下のことをする。
- 停止状態になっているSerial Dispatch Queueを再開する。追加されたタスクは順番に1つずつ実行されていく。
L:25
public struct Alamofire { // ... }
Alamofire
そのものはクラスではなくstructになっている。- Swiftにおいてstructはクラスと同様にプロパティやメソッドを持つことができたりprotocolに準拠することができる等多くの点で共通しているのだけど、structはクラスとは違って常に値渡しになり参照カウントを使わない。
L:928
最初に呼ばれるメソッドであるAlamofire.request
の実装を読む。
extension Alamofire { // ... static func request(method: Method, _ URL: String, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request { return Manager.sharedInstance.request(encoding.encode(URLRequest(method, URL), parameters: parameters).0) } }
static
とついているのは、クラスではなくstructだからclass func
ではなくstatic func
と書くのであろう。Javaみたいにstatic
に統一してもいいと思う。- 内部では
Manager
クラスのシングルトンインスタンスのrequest
メソッドを呼んでいる。 - 引数に
ParameterEncoding
インスタンスのencode
メソッドの返り値を渡している。.0
というのはtupleの要素を取り出すときにこういう書き方をする。
L:141
Manager
クラスの初期化について見る。
class Manager { class var sharedInstance: Manager { struct Singleton { static let instance = Manager() } return Singleton.instance } }
- このシングルトンパターンの実装はhpique/SwiftSingletonで推奨されているアプローチ。
- 現在はクラスにstaticな定数を定義することができない一方でstructであればそれが可能なので、ネストしたstructにシングルトンオブジェクトを定数として定義してそれを外側のクラスの型プロパティからアクセスできるようにしている。
L:208
func request(request: NSURLRequest) -> Request { // ... var dataTask: NSURLSessionDataTask? dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { dataTask = self.session.dataTaskWithRequest(mutableRequest) } let request = Request(session: self.session, task: dataTask!) self.delegate[request.delegate.task] = request.delegate request.resume() return request }
NSURLSession
オブジェクトからRequest
オブジェクトを初期化している。delegate
を登録し、Request
オブジェクトのresume
メソッドを呼んでいる。Request
オブジェクトを返している。
L:403
class Request { // ... private init(session: NSURLSession, task: NSURLSessionTask) { self.session = session if task is NSURLSessionUploadTask { self.delegate = UploadTaskDelegate(task: task) } else if task is NSURLSessionDownloadTask { self.delegate = DownloadTaskDelegate(task: task) } else if task is NSURLSessionDataTask { self.delegate = DataTaskDelegate(task: task) } else { self.delegate = TaskDelegate(task: task) } } // ... func resume() { self.task.resume() } }
Request
オブジェクトは初期化されるときに渡されたtask
のクラスに合わせてdelegate
プロパティを初期化している。is
はオブジェクトがその型に属するかどうかをチェックする。resume
メソッドはtask
プロパティ、つまりNSURLSessionTask
(またはそのサブクラスの)オブジェクトのresume
メソッドを呼び、ここで通信を開始する。
L:208
Request
オブジェクトの概要をつかんだので、request
メソッドに戻る。
func request(request: NSURLRequest) -> Request { // ... let request = Request(session: self.session, task: dataTask!) self.delegate[request.delegate.task] = request.delegate request.resume() return request }
request.delegate
は実行するtask
に応じたdelegateクラス、つまりUploadTaskDelegate
,DownloadTaskDelegate
,DataTaskDelegate
,TaskDelegate
のいずれかが入る。request.resume()
で通信を開始する。- 開始された通信が完了したときに呼ばれるdelegateは
request.delegate
であり、これはself.delegate
という領域に確保される。このプロパティはSessionDelegate
という型である。
L:229
class SessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate { private var subdelegates: [Int: Request.TaskDelegate] private subscript(task: NSURLSessionTask) -> Request.TaskDelegate? { get { return self.subdelegates[task.taskIdentifier] } set(newValue) { self.subdelegates[task.taskIdentifier] = newValue } } // ... required override init() { self.subdelegates = Dictionary() super.init() } }
SessionDelegate
オブジェクトは複数のdelegateをラップする構造をもっているようだ。- subscriptを定義することで
self.delegate[request.delegate.task] = request.delegate
のようなアクセスを実現している。内部では、キーとして渡されたRequest.TaskDelegate
オブジェクトのtaskIdentifier
を実際のキーとして使っているようだ。オブジェクトそのものではなくInt型のidentifierをキーとして使った方が効率がいいのだろう。
request
メソッドの実装についておおまかに読んだので、続いてresponse
メソッドを読んでいく。response
メソッドはrequest
メソッドの返り値であるRequest
型に対して呼ばれているので、Request
クラスの定義を調べる。
L:458
func response(completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { return response({ (request, response, data, error) in return (data, error) }, completionHandler: completionHandler) } func response(priority: Int = DISPATCH_QUEUE_PRIORITY_DEFAULT, queue: dispatch_queue_t? = nil, serializer: (NSURLRequest, NSHTTPURLResponse?, NSData?, NSError?) -> (AnyObject?, NSError?), completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { dispatch_async(self.delegate.queue, { dispatch_async(dispatch_get_global_queue(priority, 0), { let (responseObject: AnyObject?, error: NSError?) = serializer(self.request, self.response, self.delegate.data, self.delegate.error) dispatch_async(queue ?? dispatch_get_main_queue(), { completionHandler(self.request, self.response, responseObject, error) }) }) }) return self }
response
メソッドにcompletionHandler
だけ渡すと、前者のメソッドが呼ばれ内部的に後者のメソッドが呼ばれる。self.delegate.queue
プロパティはRequest.TaskDelegate
クラス(またはそのサブクラス)のプロパティであり、レスポンスの処理はこのqueueで行われるようだ。このqueueについて詳しく見ていくことにする。
L:497
private class TaskDelegate: NSObject, NSURLSessionTaskDelegate { // ... let queue: dispatch_queue_t? // ... init(task: NSURLSessionTask) { // ... let label: String = "com.alamofire.task-\(task.taskIdentifier)" let queue = dispatch_queue_create((label as NSString).UTF8String, DISPATCH_QUEUE_SERIAL) dispatch_suspend(queue) self.queue = queue } }
queue
はtask
に対して一意なラベルを持ったSerial Dispatch Queueである。- つまり、各タスクに対してキューが1つ作成される。そのキューは追加されたタスクを1つずつ順番に実行していく。
- そして、
dispatch_suspend
によってキューは停止された状態になっているため、この状態ではタスクが追加されてもすぐに実行されるわけではない。
self.delegate.queue
がどのようなキューなのか把握したのでresponse
メソッドに戻る。
L:464
func response(priority: Int = DISPATCH_QUEUE_PRIORITY_DEFAULT, queue: dispatch_queue_t? = nil, serializer: (NSURLRequest, NSHTTPURLResponse?, NSData?, NSError?) -> (AnyObject?, NSError?), completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { dispatch_async(self.delegate.queue, { dispatch_async(dispatch_get_global_queue(priority, 0), { let (responseObject: AnyObject?, error: NSError?) = serializer(self.request, self.response, self.delegate.data, self.delegate.error) dispatch_async(queue ?? dispatch_get_main_queue(), { completionHandler(self.request, self.response, responseObject, error) }) }) }) return self }
- タスクごとのキューに追加される。ただし、この段階ではキューは停止状態なのでまだ実行されない。
- 各タスクごとのキューから、グローバルキューにタスクを追加している。グローバルキューに追加されたタスクは並列に実行される。
- グローバルキューでは、通信が完了した結果を
serializer
によってシリアライズし、その結果をresponse
メソッドに渡したcompletionHandler
というクロージャに渡して今度はメインキューに追加する。メインキューに追加されたタスクはメインスレッドで実行される。 - キューにタスクを追加したら即時に自分自身を返している。こうすることで
response
メソッド(とそれに準ずるメソッド)をメソッドチェーンでつなげていくことができる。その場合、メソッドチェーンによって追加されていくタスクは各タスクのSerial Dispatch Queueによって追加された順番に実行されていく。
次に、通信が完了したあとdelegateがどのように呼ばれていくか調べる。まず、delegateオブジェクトは何か調べるため、NSURLSession
オブジェクトが初期化されている部分を読む。
L:197
class Manager { // ... required init(configuration: NSURLSessionConfiguration! = nil) { self.delegate = SessionDelegate() self.session = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: self.operationQueue) } }
- まず
NSURLSession
オブジェクトはManagerオブジェクトのプロパティである。 NSURLSession
オブジェクトのdelegateはSessionDelegate
オブジェクトとなっている。
L:229
class SessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate { // ... }
- 確かにdelegateオブジェクトに必要なprotocolに準拠している。
NSURLSessionDataDelegate
のメソッドの実装を見てみる。
L:336
func URLSession(session: NSURLSession!, dataTask: NSURLSessionDataTask!, didReceiveData data:NSData!) { if let delegate = self[dataTask] as? Request.DataTaskDelegate { delegate.URLSession(session, dataTask: dataTask, didReceiveData: data) } self.dataTaskDidReceiveData?(session, dataTask, data) }
- 上述の通り、
SessionDelegate
オブジェクトはsubdelegates
というプロパティに実際のdelegateを保持しており、独自のsubscriptからそこにアクセスできる。subdelegatesへのdelegateオブジェクトの追加はrequest
メソッド内で行われているので、そこで追加されたdelegateオブジェクトが実際に処理を行うことになる。 as?
はダウンキャストを行い失敗した場合はnilを返す。self.dataTaskDidReceiveData?
というのはOptional型のクロージャのプロパティ。どこかでセットされていればここで実行するような仕組みになっているのだと思う。
というわけで、実際にdelegateメソッドを実行しているクラスを読む。
L:598
func URLSession(session: NSURLSession!, dataTask: NSURLSessionDataTask!, didReceiveData data: NSData!) { self.dataTaskDidReceiveData?(session, dataTask) self.mutableData.appendData(data) }
- ここではあんまり大したことはしていない。
NSURLSession
オブジェクトによる通信が完了したときに呼ばれるdelegateメソッドはNSURLSessionTaskDelegate
プロトコルのURLSession(_:task:didCompleteWithError:)
というメソッドなので、これの実装を読む。
L:558
func URLSession(session: NSURLSession!, task: NSURLSessionTask!, didCompleteWithError error: NSError!) { self.error = error dispatch_resume(self.queue) }
- このメソッドは先ほどのメソッドが実装されていた
DataTaskDelegate
クラスのスーパークラスであるTaskDelegate
に定義されている。 dispatch_resume
で停止状態になっていたキューを再開し、追加されていたタスクを実行する。上述の通り、このself.queue
はタスクごとに作られたSerial Dispatch Queueであり作成直後に停止状態にしておいたもので、response
メソッド(およびそれに似たメソッド)で追加されたクロージャがここに追加されている。それらのメソッドが通信完了時によばれるdelegateでキューが再開することで順番に実行される、という仕組みになっていることが判明した。