「集合知プログラミング」を読んでる

集合知プログラミング」という本を先週から読み始めた。この本は機械学習をテーマとしていて、現実にありそうな問題(例えば、映画の評点から似ているユーザーを推薦するとか、数ある旅行プランの組み合わせから最適なプランを選択するとか)を題材にさまざまなアルゴリズムチュートリアル形式で学んでいける。登場するサンプルコードはすべてPythonで書かれているため、まずこの本を読む前に軽くPythonについて勉強した。機械学習の本というと、むずかしい数式がたくさんでてきて近寄りがたいイメージがあるのだけど、この本についてはほとんど数式は出てこないので、カジュアルに読み進められる。

まだ半分も進んでないけど、その中で一番おもしろかったのが最適化アルゴリズムの話だった。ある最適な値を求めたいとき、「となりあう値と比較して良い方を選択する」というのを繰り返していくとどこか最適な値で落ち着くはずというアルゴリズムヒルクライム)があるのだけど、これだと局所最適に陥ってしまうということを最近勉強した。つまり、全体を見渡すともっと最適な値があるのだけど、近くの値とだけ比較しているとそれを見逃してしまうということだ。また、別のアルゴリズム(模擬アニーリング)は、試行回数が少ないうちは悪い結果を受け入れ、回数を経るにつれてその悪い結果を受け入れ難くしていくことで局所最適を回避する。

これはいろんなところで当てはまりそうな考え方だなと思った。見える範囲、理解できる範囲だけで最適な選択をとろうとするとより適切な解を見落としてしまう。若いうちは結果が悪かろうともそれを受け止めることで局所最適を回避し全体最適に近づくことができるのかもしれない。

この本を読もうと思った理由としては、いろんな領域と機械学習を組み合わせるとなんか面白いものが作れそうな気がしたから。これまで自分が作ってきたソフトウェアの中で自分自身気に入っているものの多くは別の領域のアイデアを持ち込むところから生まれている。だから、組み合わせの可能性が大きい領域を何か新しく学びたいと思ったときに機械学習というものが浮かんでてきてこの本から取り組んでみることにした。今は「iOS x 機械学習」みたいな掛け合わせで何か面白いものが作れないか考えている。

集合知プログラミング

集合知プログラミング

ストリームを利用したローパスフィルタの実装

f:id:naoty_k:20140930233211p:plain

このスクリーンショットに映された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アプリのフォームの設計について本気出して考えてみた。

最もシンプルなフォーム

f:id:naoty_k:20140918011114p:plain

  • メールアドレス用の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が押された場合に実行される。今後、このメソッドにログイン処理を実装していく予定。
  • emailFieldpasswordFielddelegateをこの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:)で呼び出す。
  • UIViewControllerUIResponderを継承しており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から変更できるようになる。

f:id:naoty_k:20140918011350p:plain

次に、UIButtonもHIGの方針に従って修正する。UIButtonはサイズを自由に変更できるので、とりあえず44x44ポイントに変更した。ボタンの大きさは変更したものの、ボタンの「Login」というテキストはまだ小さいため、ユーザーの目からはサイズが大きくなったようには見えていない。そこで、ボタンにもボーダーをつけてみる。

// BorderedButton.swift

@IBDesignable
class BorderedButton: UIButton {
    @IBInspectable
    var borderWidth: CGFloat = 0 {
        didSet {
            layer.borderWidth = borderWidth
        }
    }
}

TextFieldContainerと同じようにstoryboardから枠線の幅を変えられるようにした。

マシなフォーム

以上の変更を行った結果このようになった。

f:id:naoty_k:20140918011412p:plain

間違いなくタップはしやすくなった。

問題点

  • フラットデザインに則っていない。標準のアプリや人気の高いアプリはiOS 7から導入されたフラットデザインに沿ってデザインされており、同様なインターフェイスをもたなければユーザーは慣れ親しんだ動作で直感的にアプリを操作できなくなってしまう。

改善4: フラットデザインに従う

HIGではフラットデザインの基本的な設計方針として以下の3つを挙げている。

  • 控えめであること
  • 明瞭であること
  • 奥行きを与えること

具体的な作業として

  • 画面全体を使う
  • 枠線を使わない
  • 余白を十分にとる

を意識してStoryboardを編集した。Auto Layoutで各Viewの余白を固定したり、枠線の太さを0ポイントにした。その結果、以下のようになった。

f:id:naoty_k:20140918011429p:plain

フラットデザインに対するよくある批判として「ボタンがどこにあるのか認識しにくい」というものがある。"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として実装した。
  • ショートカットのためwidthheightというcomputed propertyを用意した。

そして、Storyboardから枠線の色と幅を設定し余白を調整すると以下のようになった。

f:id:naoty_k:20140918011441p:plain

まとめ

最初と最後を比べると少しはマシなフォームになったと思う。改善したポイントをまとめると以下のようになる。

  • Return Keyで適切なアクションを起こす
  • キーボードを閉じる
  • タップしやすくする
  • フラットデザインに従う

現実の開発では、アプリごとのテーマに合わせた色やタイポグラフィを使うことになるだろうし、フォームのエラーメッセージの扱いについても触れられていない。残された課題については、経験を積む中で考えていくことにしたい。

最後に上で載せたコードを含んだプロジェクトをGitHubに公開したので参考にしてほしい。

naoty/BetterFormApp · GitHub

Swiftでストリームを扱うライブラリを書いた

FRPの記事をいくつか見てあまり理解できなかったので、Swiftでストリームを扱うライブラリを書いてみた。結論から言うと、まだストリームについて深く理解できていない感じがするので「FRPとは何か」「ストリームとは何か」といった話はしない。そういう話は他のエントリーを読んでほしいと思う。

naoty/Stream · GitHub

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

結果

  • ストリームという概念については理解できたけど、FRPという世界観の一部を理解しただけのような気がする。
  • ライブラリ書いてみたけどSwiftクロージャの循環参照らへんの実装があやしい。おかしなところがあれば指摘してもらえると助かります。
  • iOS開発においてFRPをどのように適用できるか、まだ具体的なシーンがあまり思い浮かばない。けど、ストリームというのは概念をモデル化する際の見方の一つだと思うので、そういう見方があるということを理解した上で少しずつ既存のものの見方を変えてみるとわかってくるのかもしれないと思う。

SwiftでNSDateを簡単に扱うライブラリを書いた

Swiftの実験的なプロジェクトとしてActiveSupportの拡張っぽく直感的に時間を扱うライブラリ"Timepiece"というものを書いた。

naoty/Timepiece · GitHub

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)")
        }
    }
}
  • expectationWithDescriptionwaitForExpectationWithTimeoutXcode 6からXCTestに追加された非同期テスト用のAPI
  • まず、expectationWithDescriptionメソッドXCTestExpectationオブジェクトを生成する。
  • waitForExpectationWithTimeoutは指定された秒数、上で生成されたXCTestExpectationオブジェクトのfulfillメソッドが呼ばれるのを待つ。呼ばれれば成功、呼ばれずに指定された秒数が経過すると失敗となり引数に渡されたクロージャを実行する。

他のテストコードも読んでみたけど、上と同じようなコードがあり特に読む必要はなさそうだった。

Alamofireを読んだ

Alamofireとは

使い方

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メソッドは以下のことをする。
    • NSURLSessionTaskを生成して通信を開始する。
    • 実行する通信タスクに合わせたdelegateオブジェクトを設定する。delegateオブジェクトはユニークなSerial Dispatch Queueを持つが、最初は停止状態になっている。
    • Requestオブジェクトを生成して返す。
  • responseメソッドは以下のことをする。
    • 引数に渡されたクロージャを停止状態になっているSerial Dispatch Queueに追加する。
    • 自分自身を返すため、続けて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()で通信を開始する。
  • 開始された通信が完了したときに呼ばれるdelegaterequest.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
    }
}
  • queuetaskに対して一意なラベルを持った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オブジェクトのdelegateSessionDelegateオブジェクトとなっている。

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でキューが再開することで順番に実行される、という仕組みになっていることが判明した。