[Swift] macOSでFirebaseを使う ~ Authentication編

[前回](https://qiita.com/masakihori/items/d2ac32ae0ed8f67fbbbb)の続きです

##Firebaseによる認証

###Keychain
Firebaseの認証機構はKeychainを利用するように出来ているので、プログラマがあの面倒くさい実装をする必要がありません。
すごくありがたいですね！

###仕様

0. 認証が出来るようにする
0. ユーザーを作成できるようにする
0. サインアウトもできるようにする
0. メインウインドウと、認証/ユーザー作成パネルをもつ
0. メインウインドウはサインアウトボタンのみ
0. 未認証であれば認証/ユーザー作成パネルを表示し、メインウインドウは表示しない
0. 認証済みであればメインウインドウを表示し、認証/ユーザー作成パネルは表示しない
0. ユーザーへのアラートなどは実装しない。stdoutにエラーログを出力。

よくある感じの奴です。

###Authenticationを実装
ではサクサクっと実装していきます。

####メインウインドウ
まずはメインウインドウを作成します。
xib付きでNSWindowControllerのサブクラスとしてMainWindowController.swiftを作成します。

MainWindowController.swiftはNibをロードするだけでいいので以下のようにします。

```swift:MainWindowController.swift
import Cocoa

class MainWindowController: NSWindowController {

    var nibName: NSNib.Name {
        
        return NSNib.Name("MainWindowController")
    }
}
```

NSWindowController.xibはウインドウに「サインアウト」ボタンを貼り付けて`FirstResponder`に `signOut:`アクションを作って接続します。
この後 `signOut(_:)`メソッドを作りますのでアクションの接続はそのあとでも構いません。
NSWindowの`Visible At Launch`はオフにします。

####認証/ユーザー作成パネル
メインウインドウと同じようにxib付きでNSWindowControllerのサブクラス LogInPanelController.swiftを作成します。

UIパーツとのデータのやり取りにはいろいろな方法がありますが、今回はCocoaBindingsを選びました。

<img width="485" alt="スクリーンショット 2018-07-13 19.20.39.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/2117/d2205dfe-8241-2e7e-81d1-74cfa05f036b.jpeg">
こんな感じにしました。

メールアドレス用の`NSTextField`の`value`に`File's Owner`の`mail`をbindし、`Continuosly Updates Value`をチェック、
パスワード用の`NSSecureTextField`の`value`に`File's Owner`の`password`をbindし、`Continuosly Updates Value`をチェック、
Create UserおよびLog inボタンの`enabled`に`File's Owner`の`canLogIn`をbind。
以上4ヶ所のCocoaBindingsを設定します。

ボタンのアクションは後ほど設定します。

NSWindowの`Visible At Launch`はオフにします。

ソースコードです。基本部分。

```swift:LogInPanelController.swift
import Cocoa
import FirebaseAuth

class LogInPanelController: NSWindowController {
    
    @objc dynamic private var mail: String = ""
    @objc dynamic private var password: String = ""
    
    @objc dynamic private var canLogIn: Bool {
        
//       return isValidMail() && isValidPassword()
        return mail != "" && password != ""
    }
    
    override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        
        switch key {
            
        case #keyPath(canLogIn):
            return [#keyPath(mail), #keyPath(password)]
            
        default:
            return []
        }
    }
    
    override var windowNibName: NSNib.Name {
        
        return NSNib.Name("LogInPanelController")
    }
}
```

ここまでは特に問題ないでしょう。メールアドレスとパスワードのバリデーションは手抜きです。

####ユーザーの作成

まずはコードを見てください。

```swift:LogInPanelController.swift
extension LogInPanelController {
    
    @IBAction private func createUser(_: Any) {
        
        guard canLogIn else { fatalError("ここに来るとはなさけない。") }
        
        Auth.auth().createUser(withEmail: mail, password: password) { result, error in
            
            if let error = error as NSError?, let code = AuthErrorCode(rawValue: error.code)  {
                
                switch code {
                    
                case .invalidEmail:
                    print("Mail address is invalid.")
                    
                case .emailAlreadyInUse:
                    print("Mail address is already use.")
                    
                case .operationNotAllowed:
                    print("Operation not allowed")
                    
                case .weakPassword:
                    print("Password is weak.")
                    
                default:
                    print("なんかへん。", error)
                }
                
                return
            }
            
            guard result?.user != nil else {
                
                print("Can not create user.")
                return
            }
            
            print("Success create user.")
        }
    }
}
```

`Auth`クラスの`auth()`関数からデフォルトの`Auth`のインスタンスを取得します。
通常はこのデフォルトのインスタンスのみ使用します。
`Auth`のインスタンスを介して認証に関するさまざまな処理が実行可能です。

ユーザーの作成はその名もずばり、`createUser(withEmail:,password:,completion:)`メソッドを使用します。

`completion`はユーザーの作成が成功または失敗した後にメインスレッド上で呼び出されます。
型は `(AuthDataResult?, Error?) -> Void`です。

`Error`のcodeからenum `AuthErrorCode`が作れますのでちょっとだけSwiftっぽいです。

####認証

まずはコードをみてください。

```swift:LogInPanelController
extension LogInPanelController {
    
    @IBAction private func logIn(_: Any) {
        
        guard canLogIn else { fatalError("ここに来るとはなさけない。") }
        
        Auth.auth().signIn(withEmail: mail, password: password) { result, error in
            
            if let error = error as NSError?, let code = AuthErrorCode(rawValue: error.code) {
                
                switch code {
                    
                case .invalidEmail:
                    print("Mail address is invalid.")
                    
                case .wrongPassword:
                    print("Mail or Password is wrong.")
                    
                case .userNotFound:
                    print("Not registered user.")
                    
                case .userDisabled:
                    print("User account is BAN.")
                    
                default:
                    if let email = result?.user.email {
                        
                        print("log in", email)
                    }
                }
            }
        }
    }
}
```

認証は`signIn(withEmail:,password:,completion:)`メソッドを使用します。
`createUser(withEmail:,password:,completion:)`とほぼ同じです。
問題ないでしょう。

####アクションの設定
`createUser:`と`logIn:`の両方のアクションが出来ましたので、xibで各ボタンにアクションを設定します。


###サインアウト
サインアウトが出来るように最初に見た`signOut(_:)`アクションを作成します。
これは`AppDelegate`で実装します。

```swift:AppDelegate.swift
extension AppDelegate {
    
    @IBAction private func signOut(_: Any) {
        
        try? Auth.auth().signOut()
    }
}
```

Keychainに問題があるときにthrowされることがあるようです。
今回は捨ててます。

###ウインドウの表示
ログインの状態によって表示するウインドウを変更します。
これも`AppDelegate`で実装します。

まずコードを見てください。

```swift:AppDelegate.swift
import Cocoa
import FirebaseCore
import FirebaseAuth

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    
    private let mainWindow = MainWindowController()
    
    private let loginPanel = LogInPanelController()
    
    func applicationWillFinishLaunching(_ notification: Notification) {
        
        FirebaseApp.configure()
    }
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        Auth.auth().addStateDidChangeListener { auth, user in
            
            if user == nil {
                
                self.mainWindow.window?.orderOut(nil)
                self.loginPanel.showWindow(nil)
                
            } else {
                
                self.loginPanel.window?.orderOut(nil)
                self.mainWindow.showWindow(nil)
                
            }
        }
    }
}
```

`applicationDidFinishLaunching(_:)`が該当部分です。
`addStateDidChangeListener(_:)`メソッドで関数を登録します。
この関数はユーザーのログイン状態が変化したときに呼び出されます。

関数の型は`(Auth, User?) -> Void`です。

`user`には現在のユーザを表す`User`のインスタンスが入ります。
ログインしていない場合はnilです。

これを利用して、表示するウインドウをトグルしています。

本来は`addStateDidChangeListener(_:)`メソッドの返値を保存しておいて、不要になったら`removeStateDidChangeListener(_:)`で解除する必要があるのですが、今回のアプリでは終了するまで監視し続けますので値は捨ててます。

###おわり
これで認証編はおわりです。

~~本当はプロジェクトをGithubにでもあげればいいのですが、GoogleService-Info.plistの扱いが面倒くさいのであげていません。~~

GoogleService-Info.plistを入れずにGitHubにあげました。

https://github.com/masakih/FirebaseMacOS

###次回

次回はRealtime Database編です。

[macOSでFirebaseを使う ~ 準備編](https://qiita.com/masakihori/items/d2ac32ae0ed8f67fbbbb)

[macOSでFirebaseを使う ~ Realtime Database用準備編](https://qiita.com/masakihori/items/992eb83171d6269a30ba)

[macOSでFirebaseを使う ~ Realtime Database編](https://qiita.com/masakihori/items/dbeba6f3c92208b8db9d)

[macOSでFirebaseを使う ~ おまけ](https://qiita.com/masakihori/items/9975b938a12d759d5d22)