SwiftからGoogle Cloud Endpointsで作成したLibraryを使って、Google App Engine上のAPIを呼んでみた


ココ数ヶ月Java書いてない一応Java Engineerの大橋です。
どこもかしこもswiftですね。 

iPhone系アプリ開発を一番やりたくない理由がObjective-Cだったのですが、
swiftの登場でやる気が出ました。

swiftはObjective-CのLibraryも呼べるとの事なので、
今回はGoogle Cloud Endpointsで作成したObjective-Cのクライアントライブラリを
swiftから呼び出すって話を書きたいと思います。


## まとめ

最終的には以下のコードになります。

https://github.com/soundTricker/swift-cloud-endpoint-sample

所感として

* Swift版だと楽ではある。
* Objective-Cっぽさが残るので、Cloud EndpointsからSwiftのコードを作成して欲しい
* 元々のLibrary作成までの道程が長い(ｹﾞﾌﾝｹﾞﾌﾝ
* Golangが好き

です。

## Cloud Endpointsの準備

まずSwiftうんぬんの前に以下のことが準備として必要です。
GAEをやっててGoogle系APIをよく触っている方はみたいでもわかるのでそれぞれ飛ばしても良いと思います。
なお以下の手順はObjective-C用のLibraryを作る手順なのでSwiftを使う予定が無くても使えます。

1. Google Developer Console でプロジェクトの作成
2. API用のClientId、Secretの作成
3. GAE上でCloud Endpointsを利用してAPIを作成(今回はGoを使います。)
4. 作成したAPIを元にObjective-CのLibraryを作成
5. XCodeでswiftプロジェクトを作成して、作成したLibraryとその他もろもろを読み込む
6. XCodeプロジェクトの設定(ARC周り)

### Google Developer Console でプロジェクトの作成

↓から適当に作って下さい。

https://console.developers.google.com/project

### API用のClientId、Secretの作成

作成したプロジェクトでios向けOAuth2認証用のClientIdとSecretを作成します。

1. プロジェクト画面を開いて左側メニューの「APIと認証」→「認証情報」を選択
2. 「新しいクライアントIDを作成」をクリック  
![swift1.png](https://qiita-image-store.s3.amazonaws.com/0/2965/bcbba2d7-a432-adf1-42bd-7df1ba589b49.png "swift1.png")
3. 「クライアントIDを作成」ダイアログが表示されるので、「インストールされているアプリケーション」を選択、「インストールされているアプリケーションの種類」に「iOS」を選択
4. バンドルIDはこの後作成するSwiftプロジェクト(またはObjective-Cプロジェクト)の`{組織ID}.{アプリケーションID}`を入力(Info.plistのBundle identifierの値)、その他は一旦設定しなくても大丈夫です。(クライアントアプリケーションをリリースする場合は他も設定したほうが良いはずです。)  
![ swift2.png](https://qiita-image-store.s3.amazonaws.com/0/2965/bef951db-02d9-9f64-1c9b-68173a8789ce.png " swift2.png")
5. 「クライアントIDを作成」をクリックしてclient id、client secretが発行されるのでどこかに控えておきます。  
![ swift3.png](https://qiita-image-store.s3.amazonaws.com/0/2965/3f2a1c61-51c9-4c38-ea8e-3c04083a8170.png " swift3.png")


### GAE上でCloud Endpointsを利用してAPIを作成(今回はGoを使います。)

今回は以下の2つのAPIを作成します。

ざっくり言えば /message というendpointsにGETとPOSTのAPIを作成して、
POSTの方は認証必要にしただけです。

* `/swiftsampleapi/v1/message`
  * HTTP Method : GET
  * パラメータ : 無し
  * 動作 : メッセージを返却するのみ
  * レスポンス : 以下のプロパティを持ったJSON
      * message
          * ログイン(認可済み)していない場合は"Hello World!"
          * している場合は"Hello {ログインユーザのEmailアドレス}"
* `/swiftsampleapi/v1/message`
  * HTTP Method : POST
  * パラメータ : 以下のプロパティを持ったJSON
      * message
          * 任意
  * 動作 : 渡されたメッセージをDatastoreへ保存 認可済みでない場合はエラーを返す
  * レスポンス : 以下のプロパティを持ったJSON
      * id
          * datastoreの自動で付けられたnumber型のID
      * message
          * パラメータで渡されたもの
      * email
          * ログインユーザのEmailアドレス
      * registeredAt
          * datastoreへ突っ込んだ時間

この実装は以下においてあります。

https://github.com/soundTricker/swift-cloud-endpoint-sample/blob/master/server/src/swift/main.go


[go-endpoints](https://github.com/crhym3/go-endpoints)と[goon](https://github.com/mjibson/goon)を使ってます。
`main.go`の定数でclientID、IOS_CLIENT_IDがあるので同じ値で良いので先ほど作成したclient idを設定しておきます。(client secretは後で使います。)

### 作成したAPIを元にObjective-CのLibraryを作成

ここからが糞めんどくさいです。

以下の様なことを行います。

1. GAEへデプロイ
2. discovery doc(APIの定義が書かれたJSON Schema ファイル)の取得
3. Library作成用のGenerator XCodeプロジェクトをsvn checkoutして XCodeで取り込んでビルド(え?)
4. GeneratorからLibraryを作成

公式ドキュメントは以下です。

https://developers.google.com/appengine/docs/python/endpoints/consume_ios

#### GAEへデプロイ

デプロイをしていないと2.のdiscovery docが取得できないのでデプロイします。(Goの場合です。 pythonとかJavaはしてなくてもできるはずです。 確か)
app.yamlがあるディレクトリで

```cmd
$ goapp deploy -oauth
```

を叩きます。
> ※Google Cloud SDKとGAE/GのSDKをインストールしておいて下さい

#### discovery doc(APIの定義が書かれたJSON Schema ファイル)の取得

定義ファイルを取得します。
基本的にはデプロイしたアプリケーションの以下のURLへアクセスすれば取得できます。

	https://{google projectのapp id}.appspot.com/_ah/api/discovery/v1/apis/{api名}/v1/rpc

今回だと以下の感じです。

	https://swift-cloud-endpoint-sample.appspot.com/_ah/api/discovery/v1/apis/swiftsampleapi/v1/rpc

※当面叩けるように置いておきます

> ※注意
Android側のAPIを作ったことが有る方だと時々ハマるのですが、上記の末尾のパスが`rest`のものもあります。Androidだと`rest`の方のパスを使いますが、iosの場合は`rpc`じゃないと動かないのでrpcにして下さい。

curlとかで取得する場合は以下の感じですね

```
$ URL='https://swift-cloud-endpoint-sample.appspot.com/_ah/api/discovery/v1/apis/swiftsampleapi/v1/rpc'
$ curl -s $URL > swiftsampleapi.rpc.discovery
```

#### Library作成用のGenerator XCodeプロジェクトをsvn checkoutして XCodeで取り込んでビルド(え?)

ここからが結構大変です。

1.Library作成用のGenerator XCodeプロジェクトをsvn checkout  

	$ svn checkout \
    	http://google-api-objectivec-client.googlecode.com/svn/trunk/ \
    	google-api-objectivec-client-read-only

2.XCode上で上記プロジェクトの

	google-api-objectivec-client-read-only/Source/Tools/ServiceGenerator/ServiceGenerator.xcodeproj


をインポートして、ビルドします。(Cmd + B) 
※このプロジェクトには`ServiceGenerator`というターゲットのみ存在します。


### GeneratorからLibraryを作成

ビルドすると`ServiceGenerator`という実行可能ファイルができるので以下のコマンドを叩きます。

	$ /path/to/ServiceGenerator /pathto/{先ほど作成したdiscovery doc} --outputDir ./api

以上で`./api`ディレクトリにObjective-CのCloud Endpoints Libraryが作成されます。

```tree
./api
├── GTLQuerySwiftsampleapi.h
├── GTLQuerySwiftsampleapi.m
├── GTLServiceSwiftsampleapi.h
├── GTLServiceSwiftsampleapi.m
├── GTLSwiftsampleapi.h
├── GTLSwiftsampleapiConstants.h
├── GTLSwiftsampleapiConstants.m
├── GTLSwiftsampleapiGetRes.h
├── GTLSwiftsampleapiGetRes.m
├── GTLSwiftsampleapiPostReq.h
├── GTLSwiftsampleapiPostReq.m
├── GTLSwiftsampleapiPostRes.h
├── GTLSwiftsampleapiPostRes.m
└── GTLSwiftsampleapi_Sources.m
```

### XCodeでswiftプロジェクトを作成して、作成したLibraryとその他もろもろを読み込む

XCodeでSwiftのプロジェクトを作成し、上記Libraryをプロジェクト内に放り込みます(Drag & Drop)。（Groupとか適当に作って下さい)
この時`*_Sources.m`は除いて下さい。重複エラーが出ます。

この時`{XCodeプロジェクト名}-Bridging-Header.h`というファイルを作るか聞かれるので、作って下さい。後ほど使います。

またこのLibrary群が依存するLibraryがあります。
それらは、先ほどsvn checkoutした`google-api-objectivec-client-read-only`野中の`google-api-objectivec-client-read-only/Source/GTL.xcodeproj`にあるので、
それをXCode上でプロジェクトとしてインポートし、`OAuth2/Mac`を除き全て持ってきます。

共に持ってきたら先ほど作成された`{XCodeプロジェクト名}-Bridging-Header.h`で読み込みます。

```c:`{XCodeプロジェクト名}-Bridging-Header.h`
#import "GTMOAuth2ViewControllerTouch.h"
#import "GTMHTTPFetcherLogging.h"
#import "GTLSwiftsampleapi.h"
```

### XCodeプロジェクトの設定(ARC周り)

このままだとビルドが通らないので、ARCの設定を切ります。

XCodeプロジェクトの「Build Settings」を開き、「Appple LLVM 6.0 - Languege - Objective C」の「Objective-C Automatic Reference Counting」を「No」に設定します。
※よくこの設定が私はまだ理解してないので、ちゃんと調べてやったほうがいい気がしますが...

これで準備は完了です。

## Swift側の実装

あーだこーだ言うよりコード見たほうが早い気がするのでフルコード載せます。

```swift:ViewController.swift
//
//  ViewController.swift
//  io.github.soundtricker.swift-sample
//
//  Created by soundTricker on .
//  Copyright (c) 2014 Keisuke Oohashi. All rights reserved.
//

import UIKit

class ViewController: UIViewController {


    let KEY_CHAIN_ITEM_NAME : String = "SwiftGCEApp"

    //clientId and clientSecret should be make on https://console.developers.google.com/
    let CLIENT_ID : String = "your client id"
    let CLIENT_SECRET : String = "your client secret"

    var signedIn = false

    let service : GTLServiceSwiftsampleapi

    init(coder aDecoder: NSCoder!)  {
        service = GTLServiceSwiftsampleapi()

        service.retryEnabled = true
        GTMHTTPFetcher.setLoggingEnabled(true)

        super.init(coder : aDecoder)
    }

    @IBOutlet var message : UITextField
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func saveMessage(sender : AnyObject) {
        if !signedIn {
            let alert = UIAlertController(title: "Error", message: "Is not Loggedin", preferredStyle: UIAlertControllerStyle.Alert)
            alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
            return
        }

        let param = GTLSwiftsampleapiPostReq()
        param.message = message.text

        let query : GTLQuerySwiftsampleapi = GTLQuerySwiftsampleapi.queryForMessagePostWithObject(param) as GTLQuerySwiftsampleapi

        self.service.executeQuery(query, completionHandler: { (ticket : GTLServiceTicket!, object : AnyObject!, error : NSError!) -> Void in


            if error != nil {
                NSLog("\(error)")
                return
            }
            let res  = object as GTLSwiftsampleapiPostRes
            self.message.text = "\(res.message) \(res.identifier) \(res.registeredAt) \(res.email)"
            })
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func authorize(sender : AnyObject) {
        let viewController = GTMOAuth2ViewControllerTouch(scope : "https://www.googleapis.com/auth/userinfo.email" , clientID: CLIENT_ID, clientSecret:CLIENT_SECRET, keychainItemName : KEY_CHAIN_ITEM_NAME, delegate : self, finishedSelector : "viewController:finishedWithAuth:error:")

        self.presentModalViewController(viewController,  animated: true)
    }

    @objc(viewController:finishedWithAuth:error:)
    func finishedWithAuth(viewController :GTMOAuth2ViewControllerTouch , finishedWithAuth auth:GTMOAuth2Authentication,error:NSError){
        self.dismissModalViewControllerAnimated(true)

        if error != nil {

        } else {
            self.service.authorizer = auth
            auth.authorizationTokenKey = "id_token"
            signedIn = true
        }
    }



    @IBAction func greeting(sender : AnyObject) {
        let query : GTLQuerySwiftsampleapi = GTLQuerySwiftsampleapi.queryForMessageGet() as GTLQuerySwiftsampleapi

        self.service.executeQuery(query, completionHandler: { (ticket : GTLServiceTicket!, object : AnyObject!, error : NSError!) -> Void in


            if error != nil {
                NSLog("\(error)")
                return
            }

            self.message.text = (object as GTLSwiftsampleapiGetRes).message
            
        })
    }

}

```

`init`メソッドでクライアントLibraryの起点となるServiceクラスを作成しています。
`greeting`メソッドで認証有無に関係ないAPIコールをしています。普通にコールバックがかけて楽ですね。
認証処理は`authorize`メソッドで実施しています。
もともと`@selector`というのを使っていたので、
その場合は、対応するメソッドを`@objc()`を付けた状態で定義する必要があるみたいです。
> ※`finishedWithAuth`メソッドがそれ

`saveMessage`メソッドで認可状態でのパラメータ付きPostリクエストを送ってます。
こう見るとだいぶ楽ですね。