[Swift] Firebaseを使ってGoogle/Facebookログインを実装してみた


## 経緯
 - 勉強も兼ねてFirebaseに全任せで、個人のアプリを作成中
 - まずはログイン機能をということで実装してみた

## 所要時間
ドキュメントが充実しているので、多分2hもかからないくらい簡単

## 前提条件
以下、完了していること

 - Firebaseにプロジェクト作成済

 - Xcode側での初期設定 --> [スタートガイド](https://firebase.google.com/docs/ios/setup?hl=ja)
 
## 実装

### Googleログイン
 - [Googleログイン](https://firebase.google.com/docs/auth/ios/google-signin?hl=ja)

####簡単なフロー

```
pod 'Firebase/Auth'
pod 'GoogleSignIn'
```
 1. Podfileに上記を追加
 - $ pod install と import
 - Xcodeのプロジェクトファイル修正(URLスキーム)
 - delegate処理追加
 - FirebaseのダッシュボードからGoogleサインインを有効化

####ハマりポイント
特になし！

### Facebookログイン

- [Facebookログイン(firebase)](https://firebase.google.com/docs/auth/ios/facebook-login?hl=ja)

- [FaceBook公式ドキュメント](https://developers.facebook.com/docs/facebook-login/ios)

####簡単なフロー

 ```
 pod 'Firebase/Auth
 pod 'FBSDKLoginKit'
 ```

 1. FaceBook側でアプリを新規作成(※アプリID/シークレットIDが必要)
 2. Podfileに上記を追加
 3. $ pod install と import
 4. info.plist設定
 5. delegate処理追加(これは[Facebookログイン](https://firebase.google.com/docs/auth/ios/facebook-login?hl=ja)見たほうがいい)
 - FirebaseのダッシュボードからFaceBookサインインを有効化

####ハマりポイント

 - ライブラリが最初上手くインストール出来なかったようで、ビルドエラーの嵐になった
  -> Xcode開き直して、クリーンビルドしたら直った

 - FaceBookのアプリ登録するURLがわかりづらい
  -> [Facebookログイン](https://firebase.google.com/docs/auth/ios/facebook-login?hl=ja)から、FaceBookにログインしてあげると登録出来た

## コード

```swift
import UIKit

class LoginViewController: UIViewController {
    
    // StoryboardでLoginBaseViewを制約つけて追加
    @IBOutlet weak var loginBaseView: LoginBaseView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loginBaseView.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

extension LoginViewController: LoginBaseViewDelegate {
    func loginBaseView(succeededBy type: LoginType) {
        switch type {
        case .firebase:
            print("Success FireBase")
        case .google:
            print("Success Google Login")
        case .facebook:
            print("Success FaceBook Login")
        case .line:
            print("Success Line Login")
        }
        let rootNav = StroyBoards.main.instantiateViewController(withIdentifier: "rootNav")
        self.present(rootNav, animated: true, completion: nil)
    }
    
    func loginBaseView(failedBy type: LoginType) {
        switch type {
        case .firebase:
            print("Failed FireBase")
        case .google:
            print("Failed Google Login")
        case .facebook:
            print("Failed FaceBook Login")
        case .line:
            print("Failed Line Login")
        }
    }
}
```

```swift
import UIKit
import Firebase
import GoogleSignIn
import FBSDKLoginKit

protocol LoginBaseViewDelegate: class {
    func loginBaseView(succeededBy type: LoginType)
    func loginBaseView(failedBy type: LoginType)
}

enum LoginType {
    case firebase
    case google
    case facebook
    case line
}

class LoginBaseView: UIView, GIDSignInDelegate, GIDSignInUIDelegate, FBSDKLoginButtonDelegate {
    
    weak var delegate: LoginBaseViewDelegate?
    lazy var gidSignInButton: GIDSignInButton = createGoogleButton()
    lazy var fbLoginButton: FBSDKLoginButton = createFaceBookButton()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addLoginButtons()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.addLoginButtons()
    }
    
    // MARK: - GIDSignInDelegate
    
    @available(iOS 9.0, *)
    func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
        return
            GIDSignIn.sharedInstance().handle(
                url,
                sourceApplication:options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String,
                annotation: [:]
        )
    }
    
    func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
        // ログインエラーの場合(キャンセルも含む)
        if let _ = error {
            self.delegate?.loginBaseView(failedBy: .google)
            return
        }
        guard let authentication = user.authentication else {
            return
        }
        
        // TODO: トークンはUserDefaultなりに保存して、次回以降のログインを省略
        // TODO: 何れかでログインできている場合は、ログイン画面を表示しなくて良さげ
        let credential =
            GoogleAuthProvider.credential(
                withIDToken: authentication.idToken,
                accessToken: authentication.accessToken
        )
        
        linkToFireBase(credential: credential)
    }
    
    func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) {
        // この辺の使い道がまだわかってない(※ただこれがないとエラーになる)
        print("present")
    }
    
    func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) {
        // この辺の使い道がまだわかってない(※ただこれがないとエラーになる)
        print("dismiss")
    }
    
    // MARK: - FBSDKLoginButtonDelegate
    
    func loginButton(_ loginButton: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!) {
        if let _ = error {
            self.delegate?.loginBaseView(failedBy: .facebook)
            return
        }
        
        let credential = FacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)
        linkToFireBase(credential: credential)
    }
    
    // TODO: ログアウト処理をどうするか
    func loginButtonDidLogOut(_ loginButton: FBSDKLoginButton!) {
        print("")
    }
    
    // MARK: - Private Method
    
    private func createGoogleButton() -> GIDSignInButton {
        GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
        GIDSignIn.sharedInstance().delegate = self
        GIDSignIn.sharedInstance().uiDelegate = self
        // heightとかも可変にせねば...
        let button = GIDSignInButton(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: 50))
        return button
    }
    
    private func createFaceBookButton() -> FBSDKLoginButton {
        // heightとかも可変にせねば...
        let frame = CGRect(x: 0, y: self.gidSignInButton.frame.maxY, width: self.frame.size.width, height: 50)
        let button = FBSDKLoginButton(frame: frame)
        button.delegate = self
        return button
    }
    
    private func addLoginButtons() {
        // lazyなので、参照するタイミングでcreatexxxButton()が呼ばれる
        self.addSubview(gidSignInButton)
        self.addSubview(fbLoginButton)
    }
    
    private func linkToFireBase(credential: AuthCredential) {
        Auth.auth().signInAndRetrieveData(with: credential) { (authResult, error) in
            if let _ = error {
                self.delegate?.loginBaseView(failedBy: .firebase)
            }
            // ログイン成功
            self.delegate?.loginBaseView(succeededBy: .firebase)
        }
    }
}

```

## 画面
iphone8のシミュレータ↓
![スクリーンショット 2018-10-11 0.58.06.png](https://qiita-image-store.s3.amazonaws.com/0/117388/f02301a4-6db4-702d-9ad5-6d788201cb8e.png)

## 課題
 - 各認証先(Google/Facebook)の表示を日本語に変える
 - ログイン失敗時のハンドリング(違うアプリで認証してねを促す方法)
 - ログインした後の認証情報の扱い方(1度ログインしたら、次回起動時はログインさせないみたいな)調整
 - ログアウト処理はログイン画面のアイテムからイベントがキックされないので、その調整
 - FacebookとGoogleのログインで別のアカウントが作成されてしまう問題
 - 各ボタンのデザイン調整...etc

## 参考
- [Firebase公式ドキュメント](https://firebase.google.com/docs/guides)

## 最後に
ほぼコピペでいけるくらい簡単です。
まだまだリファクタリングも必要で、課題もたくさん残っているので、そいつら直しつつLineとTwitterのログインも実装したいなぁと思ってます。
とりあえずログイン機能の実装が終わらないとスタートしないので、早めに終わらせたい！