NCMBでPodのuse_frameworks!有効時にFacebookSDKが入れられないことを解決した

# NCMBとFacebookSDK

## NCMB

NCMBとは、ニフティクラウド mobile backendのことで、
ニフティクラウドが提供するmBaaSです。SNS連携もサポートされ、Facebookログインが用意されています。
http://mb.cloud.nifty.com/

## FacebookSDK

iOSアプリでFacebookログインを使うには、通常はドキュメントにあるFacebookSDKが必要です。
http://mb.cloud.nifty.com/doc/current/sns/facebook_ios.html#Facebook SDKのインストール


# use_frameworks!

CocoaPodsでは0.36から`use_frameworks!`構文がサポートされました。
Swift 対応版 CocoaPods を使う - Qiita
http://qiita.com/taketin/items/8264aeebc5a626c6d48f
Swiftで書かれたライブラリをCocoaPods経由でインストールするには、このオプションが必須となりました。

# 問題

NCMBもFacebookSDKもそれぞれ`use_frameworks!`を指定して`pod install`までは正常に入ります。
しかし、コンパイルすると以下のIssueにあるようにエラーが出てしまっていました。
https://github.com/NIFTYCloud-mbaas/UserCommunity/issues/299
Issueを見る限りずっと解決されていない問題のようでしたが、`use_frameworks!`を外すと非常に面倒なので、`use_frameworks!`がオンのままの方法を模索しました。

# 解決方法

## ライブラリ化とサンプルアプリ

以下のレポジトリにライブラリ化し、サンプルアプリが動くようにしました
https://github.com/hiromi2424/NCMB-Facebook-use-framework

### 使い方

```swift
import NCMB_Facebook_use_framework

NCMB.setApplicationKey("APPキー", clientKey: "クライアントキー")
// ログインする
NCMBFacebookLogin(appId: "FacebookAppId").loginWithFacebook({ (user, account) in
    // ここでアカウント情報からユーザーに保存したい情報を設定する
    user.setObject(account.valueForKeyPath("properties.ACUIDisplayUsername"), forKey: "mailAddress")
}).then({ (user) -> Void in
    // userにFacebookログインで作成or取得されたNCMBUserが返ってくる
    self.user = user
}).always({
    // 成功・エラーに関わらず実行されるブロック
    self.loginButton.enabled = true
    self.refresh()
}).error({ (error) -> Void in
    // エラー表示はここでする
    self.alert(String(error))
})

```

### サンプルアプリ実行

```bash
git clone https://github.com/hiromi2424/NCMB-Facebook-use-framework
cd NCMB-Facebook-use-framework/Example
pod install
```

上記実行後、XCodeで`Example/NCMB-Facebook-use-framework.xcworkspace`を開きます。
スキーマをExampleに切り替えてビルドすることで動作を確認できます。
![スクリーンショット 2016-04-30 14.31.25.png](https://qiita-image-store.s3.amazonaws.com/0/14836/9769e9c8-714c-9aec-8a26-b751633c68db.png)

※ pod tryで実行できるようにしたかったのですが、githubレポジトリを指定する方法はPrivate Specを用意する必要がありました。時間がある時にやってみます
http://stackoverflow.com/questions/25759170/how-to-add-a-private-cocoapod-as-a-dependency-in-another-pod-podspec-file

※　前提の設定として、NCMBの管理画面で以下の設定が必要です
![Facebook設定.png](https://qiita-image-store.s3.amazonaws.com/0/14836/98ccd767-7622-0a82-064a-63e6b9397d3a.png)


## Social Framework
SwiftでFacebook連携しよう！ - Qiita iOS6で追加されたSocial.frameworkの使い方入門 - Qiita
http://qiita.com/happy_ryo/items/fef90627418b0a39a866

Social Frameworkを使い、Facebookの情報を取得します。

```swift:NCMBFacebookLogin.swift

    // FacebookアカウントをiOSのAccountsフレームワークを使って取得する
    // 取得できればACAccountのインスタンスをプロミス経由で返す
    // Facebookアカウントが複数ある場合は考慮していない（おそらくそのような状況は無い）
    public func getFacebookAccount() -> Promise<(ACAccount, ACAccountCredential)> {
        return Promise<(ACAccount, ACAccountCredential)>(resolvers: { (fulfill, reject) in
            let accountStore = ACAccountStore()
            let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierFacebook)
            var options = [String: AnyObject]()
            options[ACFacebookAppIdKey] = appId
            options[ACFacebookPermissionsKey] = permissionsKey
            options[ACFacebookAudienceKey] = audienceKey

            accountStore.requestAccessToAccountsWithType(accountType, options: options, completion: { (granted, error) in
                if granted {
                    print("Success to get facebook account")
                    let accounts: NSArray = accountStore.accountsWithAccountType(accountType)
                    if accounts.count >= 1 {
                        let account: ACAccount = accounts.lastObject as! ACAccount
                        fulfill((account, account.credential))
                    } else {
                        reject(NSError(domain: "AccountNotFound", code: 403, userInfo: nil))
                    }
                } else {
                    if error.domain == "" {

                    }
                    print("facebook login error:")
                    print(error)
                    reject(error)
                }
            })
        })
    }
```

account.credentialを参照していないと、Promiseのブロックを抜けた時credentialがnilになる現象が発生してはまりました（GC関連だと思いますが、追いきれていません）

ここで取得した情報を元にFacebookログインすれば大丈夫そうに思えますが、SocialFrameworkで取得したFacebookIDだとNCMBに`OAuth authentication error`と言われてしまいました。
デバッグの結果、Graph APIエクスプローラで取得したトークンを使ってIDを取得すると、SocialFrameworkで取得したFacebookIDと異なっていました。
https://developers.facebook.com/tools/explorer
アプリごとにFacebookIDが割り振られる仕様になったことが原因かと思います（間違っていたら教えて下さい）
以下のようにして直接GraphAPIを叩いて正しいほうのFacebookIDを取得します。

```swift:NCMBFacebookLogin.swift

    // AccountsフレームワークのFacebookIDが認証用に使えないことがあるので、GraphAPIを使ってグローバルに使えるIDを取得する
    public func getFacebookActualId(oauthToken: String) -> Promise<String> {
        let url = "https://graph.facebook.com/v2.6/me?access_token=\(oauthToken)"
        let req = Alamofire.request(.GET, url)
        return Promise<String>(resolvers: { (fulfill, reject) in
            req.responseSwiftyJSON({ (request, response, json, error) in
                if error == nil {
                    fulfill(json["id"].stringValue)
                } else {
                    reject(error!)
                }
            })
        })
    }
     
```

取得した情報を元にNCMBUserのFacebookログイン機能を使ってユーザーを取得することで、ログインすることができました。

# まとめ

要点は２点です。

- FacebookSDKが使えない時はSocialフレームワークを使う
- NCMBのFacebookログインに渡すIDを取得する為にGraphAPIで正しいFacebookIDを取得する

use_frameworks!を公式で対応してくれないのは辛かったのですが、これでFacebookログインはなんとかなりそうです！