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

PromiseKit/swiftを読んだ

PromiseKitとは

使い方

NSURLConnection.GET("http://placekitten.com/250/250").then{ (img:UIImage) in
    // ...
    return CLGeocoder.geocode(addressString:"Mount Rushmore")
}.then { (placemark:CLPlacemark) in
    // ...
    return MKMapSnapshotter(options:opts).promise()
}.then { (snapshot:MKMapSnapshot) -> Promise<Int> in
    // ...
    let av = UIAlertView()
    // ...
    return av.promise()
}.then {
    self.title = "You tapped button #\($0)"
}.then {
    return CLLocationManager.promise()
}.catch { _ -> CLLocation in
    return CLLocation(latitude: 41.89, longitude: -87.63)
}.then { (ll:CLLocation) -> Promise<NSDictionary> in
    // ...
}.then
// ...
  • thencatchクロージャを渡してメソッドチェーンしていく。これは普通のPromiseパターンと同じ。
  • エラーが発生したら最も近いcatchで補足される。

tl;dr

  • NSURLConnection+PromiseKit.swiftのようなextensionが何種類か用意されている。
    • 拡張されたメソッドは非同期処理を開始し、Promiseオブジェクトを初期化してすぐに返す。
    • 非同期処理が成功すると、fulfillerメソッドが実行される。
  • fulfillerメソッドは以下を実行する。
    • Promiseオブジェクトのstatus.Fulfilledに更新する。
    • handlersにあるクロージャをすべて実行する。
  • Promiseオブジェクトのthenメソッドを呼ぶと以下のようなクロージャhandlersに追加され、新しいPromiseオブジェクトを返す。

NSURLConnection+Promise.swift

public class func GET(url:String) -> Promise<NSData> {
    // ...
}
  • いくつかの拡張を見てみるとすべてPromise<T>を返すようになってる。
  • この返り値に対してthencatchを呼んでいるので、これらのメソッドPromiseクラスのメソッドだと考えられる。Promiseクラスについてはあとで見ていく。
public class func GET(url:String) -> Promise<UIImage> {
    let rq = NSURLRequest(URL:NSURL(string:url))
    return promise(rq)
}
  • 冒頭の使い方のところで出てきたUIImageを扱うメソッドはこれ。
  • NSURLRequestオブジェクトを作ってpromiseメソッドというのに渡して呼んでいる。
public class func promise(rq:NSURLRequest) -> Promise<UIImage> {
    return fetch(rq) { (fulfiller, rejecter, data) in
        // ...
    }
}
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
    // ...

    return Promise<T> { (fulfiller, rejunker) in
        // ...
    }
}
  • fetch内ではPromise<T>を初期化して返している。初期化時にまたもクロージャを渡している。
// Promise.swift

public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
    // ...
    body(fulfiller, rejecter)
}
  • 上のようなクロージャを受け取る初期化はこれのようだ。
  • まずbodyという引数を受け取る。bodyfulfillerrejecterの2つのクロージャを受け取ってVoidを返すクロージャ(ややこしい…)である。
  • このinitでは引数として受け取ったbodyというクロージャを実行している。bodyに渡される2つの引数はinit内で定義される内部メソッドである。
// Promise.swift

public init(_ body:(fulfiller:(T) -> Void, rejecter:(NSError) -> Void) -> Void) {
    func recurse() {
        for handler in handlers { handler() }
        handlers.removeAll(keepCapacity: false)
    }
    func rejecter(err: NSError) {
        if self.pending {
            self.state = .Rejected(err)
            recurse()
        }
    }
    func fulfiller(obj: T) {
        if self.pending {
            self.state = .Fulfilled(obj)
            recurse()
        }
    }

    body(fulfiller, rejecter)
}
  • fulfillerメソッドstate.Fulfilledに変更しrecurseを呼ぶ。
  • rejecterメソッドstate.Rejectedに変更しrecurseを呼ぶ。
  • recurseメソッドは、すべてのhandlerを実行したあと消去している。
func fetch<T>(var request: NSURLRequest, body: ((T) -> Void, (NSError) -> Void, NSData) -> Void) -> Promise<T> {
    // ...

    return Promise<T> { (fulfiller, rejunker) in
        NSURLConnection.sendAsynchronousRequest(request, queue:PMKOperationQueue) { (rsp, data, err) in
            // ...

            if err {
                rejecter(err)
            } else {
                body(fulfiller, rejecter, data!)
            }
        }
    }
}
  • Promise<T>の初期化時に引数として渡されたクロージャが実行されるので、このときに非同期通信が実行されるようだ。
  • 非同期通信が成功した場合、body(fulfiller, rejecter, data!)が呼ばれる。このbodyというクロージャfetchメソッドに渡されたもので、その中のfulfillerrejecterの2つのクロージャPromise<T>init内で定義されたメソッドである。

Promise.swift

public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
}
  • シグネチャーがジェネリクスまみれで複雑。dispatch_queue_t型と(T) -> U型を引数にとり、Promise<U>型を返すメソッドということになる。
  • TはPromiseクラスの型変数(←言い方合ってる?)であり、NSURLConnection+Promise.swiftの例で言うと、このTにはNSDataNSStringが入ってくる。
  • 例えばTNSDataの場合、第2引数のbodyは「NSDataを引数にとってUを返すクロージャ」となる。このUが例えばMKPlacemarkである場合、thenPromise<MKPlacemark>を返すことになる。
  • この返り値はPromise<T>であるため再度thenを呼び出すことができメソッドチェーンが成立している。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    case .Rejected(let error):
        // ...
    case .Fulfilled(let value):
        // ...
    case .Pending:
        // ...
    }
}
  • statePromise<T>クラスのプロパティでState<T>型として定義されている。
enum State<T> {
    case Pending
    case Fulfilled(@autoclosure () -> T)
    case Rejected(NSError)
}
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    case .Rejected(let error):
        // ...
    case .Fulfilled(let value):
        // ...
    case .Pending:
        // ...
    }
}
  • stateenum型であることが分かったので、thenに戻る。
  • このswitch文ではvalue bindingsを行っている。マッチしたcase文で宣言された変数に値が割り当てられる。例えば、.Fulfilledにマッチした場合、stateを初期化する際に.Fulfilledに渡されたクロージャvalueという変数に割り当てられる。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    // ...
    case .Pending:
        return Promise<U>{ (fulfiller, rejecter) in
            // ...
        }
    }
}
  • statusは宣言時に初期値として.Pendingを渡しているため、最初は.Pendingのcase文を通ることになりそう。
  • status.Pendingである場合、Promise<U>を初期化して返している。
  • 初期化の際、引数にクロージャを渡している。上述の通り、渡されたクロージャは初期化処理の最後に実行される。
public func then<U>(onQueue q:dispatch_queue_t = dispatch_get_main_queue(), body:(T) -> U) -> Promise<U> {
    switch state {
    // ...
    case .Pending:
        return Promise<U>{ (fulfiller, rejecter) in
            self.handlers.append{
                switch self.state {
                case .Fulfilled(let value):
                    fulfiller(value())
                case .Rejected(let error):
                    dispatch_async(onQueue){ fulfiller(body(error)) }
                case .Pending:
                    abort()
                }
            }
        }
    }
}
  • Promise<U>の初期化の最後でself.handlersクロージャが追加されている。上述の通り、handlersfulfillerrejecter内で呼ばれるrecurseですべて実行される。
  • つまり、then()に渡されたクロージャhandlersに追加され、そのPromiseオブジェクトの非同期処理が完了したときに呼ばれることになる。

バックエンドAPI用のテンプレートを作り始めた

モバイルアプリケーションやJavaScriptアプリケーションのバックエンドとして使うAPIのテンプレートを作り始めた。

https://github.com/naoty/metallic

まだそんなにできてないけど、連休終わるので進捗を書いておく。まだ公開できるレベルではないので公開はしてない。

動機

iOSアプリのバックエンドをサクッと作りたい、でも単なるデータストアとしてではなくて少しロジックを実装したい、ってときにMBaaSを使うよりも自分でサーバーサイドを実装したくなる。そのとき、RailsSinatraかを選ぶことになる。Railsでももちろん問題ないのだけど、必要十分な処理さえしてくれればいいという思いからSinatraを選びたくなる。だけど、Sinatra単独でバックエンドを実装するのにはかなり時間がかかる。例えば、データベースとの接続やマイグレーションの管理、JSONのパースと出力などなど、地味に大変な実装をこなさないといけない。そこで、SinatraベースでバックエンドAPIを実装するためのテンプレートを作ることにした。

現時点での機能

  • metallic new APPLICATION_NAME: テンプレートからプロジェクト作成する。
  • metallic generate controller RESOURCE_NAME: テンプレートからコントローラーを作成する。コントローラーはSinatra::Baseを継承したRESTful APIを持つクラス。作成されたコントローラーは自動的にRackミドルウェアとしてuseされる。
  • metallic generate model RESOURCE_NAME: テンプレートからモデルとマイグレーションを作成する。今のところORMはActiveRecord固定で、DBもSQLite3固定になってる。ここはRailsみたいにオプションで切り替えられるようにしたい。Rakefileもテンプレートについてくるので、そのままrake db:migrateできる。

使用例

$ metallic new todo
      create  todo/Gemfile
      create  todo/Rakefile
      create  todo/app/application.rb
      create  todo/config.ru
      create  todo/config/database.yml
$ cd todo
$ bundle install
$ metallic generate controller tasks
      create  app/controllers/tasks_controller.rb
$ metallic generate model Task
      create  app/models/task.rb
      create  db/migrations/20140721224324_create_tasks.rb
$ rake db:migrate
== 20140721224324 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0011s
== 20140721224324 CreateTasks: migrated (0.0012s) =============================
$ rackup
[2014-07-21 22:43:34] INFO  WEBrick 1.3.1
[2014-07-21 22:43:34] INFO  ruby 2.1.2 (2014-05-08) [x86_64-darwin13.0]
[2014-07-21 22:43:34] INFO  WEBrick::HTTPServer#start: pid=20212 port=9292
$ curl http://localhost:9292/tasks
GET /tasks

実装予定

  • 各種必要なRackミドルウェア: bodyのJSONをパースするヤツ、パラメータをパースしてヘルパーからページ番号やソートにアクセスできるようにするヤツ、例外をキャッチして適切なステータスコードを返すヤツ、整形されたJSONを返すヤツ、などをRackミドルウェアとして実装したい。
  • 他DB対応: generateコマンドのオプションでテンプレートを切り替えられるようにしたい。
  • あと、方針を決めかねているけど、Railsと組み合わせて使えるようにしたい。というのも、モバイルアプリケーションのバックエンドとしてだけじゃなくてJavaScriptアプリケーションのバックエンドとしても使えるようなものを目指しているので、部分的にmetallicアプリケーションをRailsアプリケーションにマウントさせる、みたいな使い方も考えられそう。なので、そういう場合を想定したテンプレートを考えたい。

APNsの概要と関連ツール群

iOS

Apple Push Notification Service(APNs)は、ソフトウェア開発者(プロバイダ)から受け取ったメッセージを安全な方法でデバイスにプッシュ通知するサービスである。

プッシュ通知までの流れ

  1. プロバイダはデバイストークンとペイロードから成る通知メッセージを作る。
  2. プロバイダはその通知メッセージをAPNsに送信する。
  3. APNsは受け取った通知メッセージのデバイストークンから配信先のデバイスを特定し、通知メッセージを配信する。

接続を確立するまでの流れ

  1. APNsとデバイス間で認証を行う(システムによって行われるため、開発者が実装する必要はない)。
  2. APNsとプロバイダ間で認証を行う。
    1. プロバイダがAPNsからサーバ証明書を取得し、検証する。
    2. プロバイダがプロバイダ証明書をAPNsに送信する。
  3. デバイストークンを生成しプロバイダと共有する。
    1. アプリケーションがリモート通知の登録を行う。
    2. システムがリモート通知の設定を行い、デバイストークンをアプリケーションデリゲートに渡す。
    3. アプリケーションがデバイストークンをプロバイダに送信する。
  4. プロバイダからの通信すべてにデバイストークンを添付させる。

APNs関連ツール

AFNetworkingでおなじみのmatttさんが様々な関連ツールを開発している。それぞれのツールが上で説明した全体像の中でどのような位置づけなのか整理した。

houston

https://github.com/nomad/houston

プロバイダからAPNsに向けて通知メッセージを送るためのクライアント。Ruby製。上記の通り、プロバイダがAPNsにメッセージを送るには、(1)APNsとの間で認証を行う、(2)配信先のデバイストークンを取得する必要がある。なので、houstonでもメッセージを送る際にAPNsのサーバー証明書と配信先のデバイストークンを設定する必要がある。

rack-push-notification

https://github.com/mattt/rack-push-notification

iOSアプリからデバイストークンを受け取りDBに保存するRackアプリケーション(Sinatraベース)。上述の通り、iOSアプリはAPNsからデバイストークンを取得したあと、プロバイダと共有する必要がある。rack-push-notificationはそのデバイストークンを受け取るためのAPIを用意する。

Orbiter

https://github.com/mattt/Orbiter

iOSアプリからデバイストークンを送信するためのクライアント。今までの2つのツールはプロバイダ側のツールだったが、OrbiterはiOSアプリ側のツールである。取得したデバイストークンをプロバイダに送信する処理を簡略化できるようだ。

まとめ

これらの関連ツールを使ったプッシュ通知のフローは以下のようになる。

  1. iOSアプリはリモート通知の登録を行い、APNsからデバイストークンを取得する。
  2. iOSアプリはOrbiterを使ってデバイストークンをプロバイダに送信する。
  3. プロバイダはrack-push-notificationを使って用意したAPIからデバイストークンを受け取りDBに保存する。
  4. プロバイダはhoustonを使ってプッシュ通知をAPNsに送信する。
  5. APNsはプロバイダ証明書とデバイストークンからプッシュ通知を転送するデバイスを特定しプッシュ通知を送る。

参考

pecoでハッカーを検索

かなり前にcui-about.meというサービスを作ったんだけど、pecoと相性がいいことに気づいたので組み合わせてみた。

f:id:naoty_k:20140704175901g:plain

about() {
    if [ $# -eq 0 ]; then
        local name=$(curl -s cui-about.me/users | peco)
    else
        local name=$1
    fi
    curl cui-about.me/$name
}