Swift：FirestoreとFacebookの連携＋Facebookのユーザーデータ取得


※Firebaseにプロジェクトは作成済みとします。

# 下準備
まずはFacebookログインに関するDocumentや記事を読みながら、Facebookログインを実装するつもりで諸々の手続きを行います。

自分が知らないだけかもしれませんが、[iOS用Facebookログイン - クイックスタート](https://developers.facebook.com/docs/facebook-login/ios)というFacebookの公式の記事は途中からObject-C言語での記述が出てきてチンプンカンプンになりました。

一連の手続きに関しては、[iOSでFirebase AuthenticationをFacebookログインでやってみる](https://qiita.com/kajitack/items/7984b42872384db81c38)という記事がとても参考になったので紹介しておきます。

簡単にですが手続きを書きます

- Facebook Developerに登録 <font color="#088A68">//TwitterのDeveloper登録と異なり、あっけなく登録できました。</font>
- アプリを作成 <font color="#088A68">//Display NameはSetting＞Basicで後から変更できます。</font>
- Setting＞BasicからApp IDとApp Secretを控えておく
- Firebase Authentication＞ログイン方法にて、FacebookのAuthを有効にする  <font color="#088A68">//この際App IDとApp Secretの入力が必要になる。その入力画面の下に OAuth リダイレクト URI があり、それも控えておく必要がある</font>
- Firebase Authentication＞ログイン方法の、FacebookのAuthを有効にする画面下部の"OAuth リダイレクト URI"を控えておく
- Facebook Developers＞Products＞Facebook Login＞Client OAuth Settings＞Valid OAuth Redirect URIsに、先ほどの"OAuth リダイレクト URI"を記述する<font color="#088A68">//最初のアプリの作成の仕方によっては、Productsがないかもしれません。その場合はダッシュボードからFacebook Loginを追加するっぽいです。</font>
- FacebookSDKをpod installしていく <font color="#088A68">//version4.39にはBugが残っているらしく、4.38以上4.39未満というversion指定をしました。</font>

```Podfile
pod 'FBSDKLoginKit', '~> 4.38.0'
pod 'FBSDKCoreKit', '~> 4.38.0'
```
参考：[Facebook Login Error - Unknown Error building URL (com.facebook.sdk.core error 3)](https://stackoverflow.com/questions/53733275/facebook-login-error-unknown-error-building-url-com-facebook-sdk-core-error-3)
なお、Firebase Authは既にインストールしているものとします。

- info.plistの編集 <font color="#088A68">//ところどころ、控えていたApp IDを記述する必要があります。記述しないとうまく動作しません。</font>
- AppDelegateの編集 

<font color="#088A68">//公式のFacebook Documentではなく、[iOSでFirebase AuthenticationをFacebookログインでやってみる](https://qiita.com/kajitack/items/7984b42872384db81c38)のAppDelegateが参考になります。</font>

# FirestoreとFacebookを連携させる
イメージとしてはNewspicksの外部サービス連携
<img width="397" alt="スクリーンショット 2019-01-13 13.29.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/261020/8dbdb5fb-d118-c493-1194-e99ea087598b.png">

シンプルにFacebookにログインさせたい場合は、たくさん記事がWeb上に転がっているので調べてみてください。
自分の場合は、Firebase Authenticationにて、匿名ログインを許可に設定し、
デフォルトの状態では匿名ログインさせておいて、希望者のみFacebookと連携させたいという方針がありました。
FBSDKLoginButton()を使ったり、signInAndRetrieveData(with: credential)を最初試してみたものの、イマイチ自分の目指すような挙動をすることはありませんでした。
<font color="#088A68">//FBSDKLoginButton()には、よくあるFacebookログインボタンが初めから用意されていて、デザインの変更などが効かない
//signInAndRetrieveData(with: credential)はログイン・ログアウトのタイミングで全く新しいuidが生成されてしまって、データが引き継げなかった。</font>

そこで使用したのが、
FBSDKLoginManager()と、linkAndRetrieveDataです。
FBSDKLoginManager()ではログイン処理を実装できます。ユーザー情報を取得するに当たって重要なpermissionも記述できます。
linkAndRetrieveDataはFirebaseの公式Document：[iOS で Firebase 匿名認証を行う](https://firebase.google.com/docs/auth/ios/anonymous-auth?hl=ja)に書かれています。

```SocialNetworkViewController.swift
import UIKit
import Firebase
import FirebaseFirestore
import FirebaseAuth
import FBSDKCoreKit
import FBSDKLoginKit
```
上記は追加するモジュールです。

```SocialNetworkViewController.swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let loginManager = FBSDKLoginManager()
    if let user = Auth.auth().currentUser {
        if user.providerData.isEmpty {
            loginManager.logIn(withReadPermissions: ["public_profile","email"], from: self) { (result, error) in
                if (error != nil) {
                    print("permission Errorメッセージ：\(error!)")
                        
                }
                    
                if let token = FBSDKAccessToken.current() {
                    let credential = FacebookAuthProvider.credential(withAccessToken: token.tokenString)
                    user.linkAndRetrieveData(with: credential, completion: { (authResult, error) in
                        if error != nil {
                            print("ログイン Errorメッセージ：\(error!)")
                            return
                        }
                        self.tableView.reloadData() //tableView更新して表示を変える
                        self.fetchUserProfile() //ユーザーデータを取ってくる
                    })
                }
            }
            return
        } else {
            for item in user.providerData {
                if item.providerID == "facebook.com" {
                    //ログアウト処理です。unlinkだけでは完全にログアウトしたことにはならないようなので要注意です。
                    loginManager.logOut()
                    user.unlink(fromProvider: "facebook.com") { (user, error) in
                        self.tableView.reloadData()
                        return
                    }
                        
                } else {
                    return
                    //Twitterとかとは連携しているが、Facebookとは連携していないケースに、Facebookとの連携を行う操作（省略）
                }
            }
        }
    } else {
        print("Auth.auth().currentUserはnilです")
    }
}

```
userの型はFIRUserでプロパティとしてproviderDataを持っています。Facebookへログインすると、providerDataのプロパティであるproviderIDに、"facebook.com"が追加されます。
ですので、上のコードでは、providerDataの有無で場合を分けて、Facebookにログインしていない場合はログイン処理をするというコードになります。
            
```swift
loginManager.logIn(withReadPermissions: ["public_profile","email"], from: self) { (result, error) in

}
```
この部分がログイン処理になります。permissionsは、["public_profile","email"]で大丈夫です。
["picture","name"]などと書いてもエラーになります。なぜなら"public_profile"の中に、defaultで引っ張ってこれるユーザーデータは含まれているからです。詳しくは[Facebookのdocument](https://developers.facebook.com/docs/facebook-login/permissions/)参照です。




# Profile情報をfetchする
fetchUserProfile()の中身を書いていきます。

```SocialNetworkViewController.swift
func fetchUserProfile(){
    let graphPath = "me"
    let parameters = ["fields": "id, name, picture"]
    let graphRequest = FBSDKGraphRequest(graphPath: graphPath, parameters: parameters)
        
    var userInfo:[String : AnyObject] = [:]

    graphRequest?.start { (connection, result, error) in
        if let error = error {
            print(error.localizedDescription)
        } else {
            userInfo = result as! [String : AnyObject] 

            if let user = Auth.auth().currentUser {
                let userRef = Firestore.firestore().collection("users").document("\(user.uid)")
                userRef.updateData(userInfo){ err in
                    if let err = err {
                        print("Error adding document: \(err)")
                    } else {
                        print("Document successfully written!")
                    }
                }
            }
        }
    }
}
```

Facebookにログインした後は、Graph APIを使ってデータを取得していきます。
こちらの記事：[How to use Graph API to retrieve Facebook user information in iOS](http://hoangtran.me/ios/development/2016/08/20/use-graph-api-to-retrieve-facebook-user-information-in-ios/)が参考になりました。

```swift

//Graph APIを初期化
let graphPath = "me"
let parameters = ["fields": "id, name, picture"]
let graphRequest = FBSDKGraphRequest(graphPath: graphPath, parameters: parameters)

graphRequest?.start { (connection, result, error) in
    if let error = error {
        print(error.localizedDescription)
    } else {
        print(result)
    }
}

```

Firestoreへアップロードするために、細工をしていきます。
まずは、userInfoというDictionaryを用意し、resultをキャストして入れます。

```swift

var userInfo:[String : AnyObject] = [:]

let graphPath = "me"
let parameters = ["fields": "id, name, picture"]
let graphRequest = FBSDKGraphRequest(graphPath: graphPath, parameters: parameters)

graphRequest?.start { (connection, result, error) in
    if let error = error {
        print(error.localizedDescription)
    } else {
        userInfo = result as! [String : AnyObject]
    }
}
```
そして、graphRequest?.startのcallback関数の中に、データをFirestoreへアップロードする処理を記述していきます。

```swift
graphRequest?.start { (connection, result, error) in
    if let error = error {
        print(error.localizedDescription)
    } else {
        userInfo = result as! [String : AnyObject] 
        if let user = Auth.auth().currentUser {
            let userRef = Firestore.firestore().collection("users").document("\(user.uid)")
            userRef.updateData(userInfo){ err in
                if let err = err {
                    print("Error adding document: \(err)")
                } else {
                    print("Document successfully written!")
                }
            }
        }
    }
}
```
graphRequest?.startのcallback関数の外に、updateData(userInfo)を記述していたら、userInfoの中身が認識されないのかアップロードできませんでした。callback関数の中に記述すれば無事に更新できます。


# Firestoreに保存したpictureを引っ張ってくる
idやnameを使う際には特に苦戦しなかったのですが、pictureを扱う際にFirestore独特のmap（マップ）というデータ型の扱いに少し苦戦したので、そちらも書いておきます。
なお、ここでいうmap（マップ）は、メソッドとしてのmapではありません。
<img width="514" alt="スクリーンショット 2019-01-13 16.40.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/261020/caf882a0-7254-2432-ba72-4300a02914fd.png">
上記画像はFirebase Firestoreのマップ型のデータです。


```ProfileViewController.swift
func fetchProfileData() {
    if let user = Auth.auth().currentUser{
        let ref = Firestore.firestore().collection("users").document("\(user.uid)")
        ref.getDocument { (document, error) in
            if let document = document, document.exists {
   
                //こちらは[String:AnyObject]の辞書型なので、["name"]でkeyを指定してあげれば、valueを取り出せる。その際にString型にダウンキャストする。
                if let nameLabelText = document.data()!["name"] as? String {
                        self.nameLabel.text = nameLabelText
                    }

                //pictureの方は階層構造になっていて、一つ一つキャスティングして掘っていく必要があります。そのままではAnyObject型なので、document.data()!["picture"]["data"]["url"]という風に指定しようとするとエラーが起きます。
                if let pictureData = document.data()!["picture"] as? [String : Any] {
                    let pictureProperties = pictureData["data"]! as! [String:Any]
                    let imageUrl = pictureProperties["url"] as! String
                    self.profileImage.sd_setImage(with: URL(string: imageUrl), placeholderImage: UIImage(named: "profile"))
                }

            } else {
                print("Document does not exist")
            }
        }
    }
}
```
エラー処理の書き方はもっと良い方法があるかもしれませんが、とりあえずオプショナルバインディングでこなしました。


# （追記）Facebookアプリを公開する
以上で連携はできるのですが、そのままではApp Storeに申請してもリジェクトされます。
なぜなら、連携させているFacebookアプリが開発者モードで公開されていないからです。
β版アプリをテストユーザーに触ってもらったり、アプリを申請する段階になったら、FacebookのDeveloperページにて、まずプライバシーポリシーの書かれたURLを記入した上で、画面上部のステータスを「開発モード」から「公開モード」変更します。
そうすれば、自分以外のユーザーも連携することができるようになります。