MeteorのDDPをiOSから使う


![lb.gif](https://qiita-image-store.s3.amazonaws.com/0/3844/de94d81b-0644-e6a6-5041-4d7b162beec4.gif "lb.gif")


#はじめに

「Meteor」というフレームワークってご存知でしょうか？
[こちらにて紹介させていただいています。](http://qiita.com/abe00makoto/items/cf7a57face7a0e264881)
リアルタイムWEBアプリケーションフレームワークであり、リアクテイブフレームワークです。

個人的に上記２点が自分の惹かれるポイントなわけですが、
問題は基本的にWebsocketによる双方向通信を行なっており、
WebAPIは用意されていない点がネイティブクライアントアプリとの接続に問題がありそうです。

しかし、Meteorは独自にWebScoket上に[DDP](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md)というプロトコルを定義しています。

しかもiOS用にクライアントが開発されているようでしたので使ってみることにしました。

ですが、まずはDDPってどんな通信をしているか見てみましょう


# DDPの通信をみる

Meteorのインストールは完了しているものとします。
すごく簡単ですので、是非[ここ](http://ja.discovermeteor.com/chapters/getting-started/)から試してみてほしいです。


## DDPを監視するためのツールをいれて動作を見てみる
[Meteor DDP Analyzer](https://github.com/arunoda/meteor-ddp-analyzer)を導入します。

```sh:install
npm install -g ddp-analyzer
```

npmもインストール済みの前提です。(``brew install npm``とかで入れましょう)

適当なアプリとしてexampleから``leaderboard``を導入してみます。


```sh:exampleinstall
meteor create --example leaderboard
cd leaderboard
```


ここでmeteorと入れればアプリが起動しますが、DDP proxyを間に挟むため以下のように入力します。

```sh:
export DDP_DEFAULT_CONNECTION_URL=http://localhost:3030
meteor
```
これで、アプリが起動したと思いますが、ddp-analyzerを別タブでterminalを開き起動します。

```sh:別タブのTerminalから起動
ddp-analyzer-proxy
```
上記TerminalからDDP入出力のログが見えるようになります。

ブラウザから[http://localhost:3000](http://localhost:3000)にアクセスしてみてください

![Leaderboard.png](https://qiita-image-store.s3.amazonaws.com/0/3844/67e6a6ff-c501-201b-1491-5ad3500f204a.png "Leaderboard.png")

複数画面からみるとサーバサイドと連動してるのがわかると思います。

```json:ログ
 2  IN   0  {"server_id":"0"}
 2  OUT  194  {"msg":"connect","version":"1","support":["1","pre2","pre1"]}
 2  IN   55  {"msg":"connected","session":"SiGpwuWFvxHbBfoKT"}
 2  IN   5  {"msg":"added","collection":"players","id":"e2iqRNh7WuHH2q7dv","fields":{"name":"Ada Lovelace","score":30}}
 2  IN   6  {"msg":"added","collection":"players","id":"4XbrBYFfGhdmkne6S","fields":{"name":"Grace Hopper","score":50}}
 2  IN   11  {"msg":"added","collection":"players","id":"KoLMxcq8cotTNMAia","fields":{"name":"Marie Curie","score":40}}
 2  IN   0  {"msg":"added","collection":"players","id":"drmtfDKhPFZzdoKcH","fields":{"name":"Carl Friedrich Gauss","score":20}}
 2  IN   1  {"msg":"added","collection":"players","id":"6FNTfH4yniGQyC45Q","fields":{"name":"Nikola Tesla","score":55}}
 2  IN   0  {"msg":"added","collection":"players","id":"7NDd96fhJKgoke7Qq","fields":{"name":"Claude Shannon","score":30}}
 2  OUT  11  {"msg":"sub","id":"WSoxxLbKaxBiTBEDc","name":"meteor_autoupdate_clientVersions","params":[]}
 2  IN   10  {"msg":"added","collection":"meteor_autoupdate_clientVersions","id":"P66dYxYKmH8ud6Y7i","fields":{"current":true}}
 2  IN   2  {"msg":"added","collection":"meteor_autoupdate_clientVersions","id":"version","fields":{"version":"9c9878112ad755389718cb18711718b4bf0cb5e0"}}
 2  IN   2  {"msg":"added","collection":"meteor_autoupdate_clientVersions","id":"version-cordova","fields":{"version":"51af6ca65d12bb7019f8483d5a4f44eaaadf2ac8","refreshable":false}}
 2  IN   4  {"msg":"added","collection":"meteor_autoupdate_clientVersions","id":"version-refreshable","fields":{"version":"96a1d99467ffa8e9f508b65850bebaf9f8396957","assets":{"allCss":[{"url":"/de4f33c8b52b307376a0d274d7bc1dfeddede1ed.css"}]}}}
 2  IN   2  {"msg":"ready","subs":["WSoxxLbKaxBiTBEDc"]}
 1  IN   29860  {"msg":"ping"}
 2  OUT  19184  {"msg":"method","method":"/players/update","params":[{"_id":"e2iqRNh7WuHH2q7dv"},{"$inc":{"score":5}},{}],"id":"1"}
 2  IN   40  {"msg":"result","id":"1","result":1}
 1  IN   121  {"msg":"changed","collection":"players","id":"e2iqRNh7WuHH2q7dv","fields":{"score":35}}
 2  IN   44  {"msg":"changed","collection":"players","id":"e2iqRNh7WuHH2q7dv","fields":{"score":35}}
 2  IN   4  {"msg":"updated","methods":["1"]}
 2  IN   10677  {"msg":"ping"}
 2  OUT  12  {"msg":"pong"}
 ```
 
 実際の操作と照らしわせると大体の動きがわかって面白いと思います。
 
 ログから察するに``players``というコレクションがあり、フィールドは
 ``name``,``score``のようです。
 ``/players/update``というメソッドから値の更新を行っているみたいですね。
 
 
 これをiOS版として実装してみます。
 
# ObjectiveDDPを試す。

DDPの仕様は公開されており各言語実装があります。
参考:http://meteorpedia.com/read/DDP_Clients

このうちiOS版実装を行っている[`ObjectiveDDP`](https://github.com/boundsj/ObjectiveDDP)を試してみます

 
実際に先ほどのサンプルである`leaderboard`をiosネイティブとして実装してみました。
![lb.gif](https://qiita-image-store.s3.amazonaws.com/0/3844/de94d81b-0644-e6a6-5041-4d7b162beec4.gif "lb.gif")

https://github.com/m0a-ios/leaderboard_ios

結論から言えば、Meteorが自動的にやってくれるデータバインディングを手動で行うイメージでした。

まずはデリゲート側でDDPへのアクセスURLの設定と、サブスクリプションの設定を行います。

今回は`players`コレクションを全公開しているだけなので、それをサブスクリプション設定して取り込むようにします。


```swift:AppDelegate.swift
//省略
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    //接続先URLには事前にデプロイした環境を設定しています
    var meteorClient = initialiseMeteor("pre2", "wss://m0a_test.meteor.com/websocket");

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    //playersコレクションを同期対象に設定
	meteorClient.addSubscription("players");	return true
    }
    
//省略
```

デリゲートに設定したMeteorクライアントから同期されたCollectionを取り出し、
監視対象に追加します。

```swift:TableViewController.swift

//省略

class TableViewController: UITableViewController {
    var players:M13OrderedDictionary!; //順序性を保持するDictinary
    var meteor:MeteorClient!; 
    var selectedIndex = -1
    @IBOutlet weak var plusButton: UIBarButtonItem!
    
    
    required init(coder aDecoder: NSCoder) {
        self.meteor = (UIApplication.sharedApplication().delegate as AppDelegate).meteorClient
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()        
        //監視対象に追加
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "didReceiveUpdate:", name: "added", object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "didReceiveUpdate:", name: "changed", object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "didReceiveUpdate:", name: "removed", object: nil)
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    
    //サーバサイドからの変更を含め値の変更を検知すると実行されます
    func didReceiveUpdate(notification:NSNotification){
        if (self.meteor.collections.objectForKey("players") != nil){
            
            self.players = self.meteor.collections["players"]  as M13OrderedDictionary
            
            //並び替え
            self.players = self.players.sortedByObjectsUsingComparator({ (objA, objB) -> NSComparisonResult in
                var scoreA = objA["score"] as Int
                var scoreB = objB["score"] as Int
                if (scoreA > scoreB ) {
                    return NSComparisonResult.OrderedAscending
                }
                if (scoreA < scoreB) {
                    return NSComparisonResult.OrderedDescending
                }
                return NSComparisonResult.OrderedSame
                
            })

            //Tableへ反映
            self.tableView.reloadData()
            self.tableView.selectRowAtIndexPath(NSIndexPath(forRow: self.selectedIndex, inSection: 0), animated: true, scrollPosition: UITableViewScrollPosition.Middle)
        }
        
    }



    @IBAction func clickPlusButton(sender: AnyObject) {
    
        if (self.selectedIndex < 0 || self.players == nil) {
            return
        }
        let obj: NSMutableDictionary = self.players[UInt(self.selectedIndex)] as NSMutableDictionary
        var _id = obj["_id"] as String
        
        //Meteorメソッドコール(MeteorにおけるRPCです)
        meteor.callMethodName("/players/update", parameters: [["_id" : _id ],["$inc":["score":5]]],
            responseCallback:
            { (NSDictionary response, NSErrorPointer error) in
                println(response)
                println(error)

            }
        )
    }
    
    
    //Tableへの表示処理
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        var cell = tableView.dequeueReusableCellWithIdentifier("testCell", forIndexPath: indexPath) as UITableViewCell
        
        let obj = self.players[UInt(indexPath.row)] as NSMutableDictionary?;
        var name = obj?["name"] as String
        var score = obj?["score"] as Int
        cell.textLabel?.text = "\(name) point: \(score)"
        return cell
    }
    
    override func tableView( tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if (self.players == nil) {
            return 0
        }
        
        return Int(self.players.count())
    }
    
    
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        selectedIndex = indexPath.row
    }
    
}

```

とりあえず、やってみたというだけでした。