【Swift】Sign in with Appleの実装について

# 概要
- この記事は僕が学習時に挑戦するときに詰まってしまったので記事におこして自分のメモ用にと初学者の手助けになればと思い記事にしました。
- 実装できたらログインが簡単なのでユーザーにも優しいアプリ作成ができます。
- Appleログインを実装するにたりApple developerの有償の登録が必要です。

# 手順１ Apple developerでアプリの登録

先ずは、下記の画像の赤枠をクリックします。
![スクリーンショット 2021-07-08 8.14.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/b8ac6779-309b-f94c-263d-0c5d8dad28e2.png)

ログインして頂き①を選択して下さい。
![スクリーンショット 2021-07-08 8.14.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/9bf59e4a-3d56-4e2b-a029-dea1a02c2902.png)

下記の画像の様に移動しますので「Identifiers」を選択して下さい。
![スクリーンショット 2021-07-08 8.14.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/a97ef2a4-a2f9-1468-26b5-cc2b4e860e3a.png)

移動すると登録しているアプリが表示されます。
そこで赤枠のプラスボタンをクリックして下さい。
![スクリーンショット 2021-07-08 8.14.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/d0a36645-4c7a-88f8-5f11-647139d62a4f.png)

「App IDs」が選択されているのを確認して赤枠の「Continue」をクリックして下さい。
![スクリーンショット 2021-07-08 8.15.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/c396a994-9e9f-fe7b-1f29-99fbee9395ae.png)

「App」が選択されていることを確認して赤枠の「Continue」をクリックして下さい。
![スクリーンショット 2021-07-08 8.15.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/d0fe31ae-4459-7d9c-c1d4-7332efa9ac2f.png)

下記の画像の様な画面になりますので画像2の様に「Description」には分かりやすい様にご自身で作成したプロジェクト名をつけて頂き「Bundle ID」は作成したプロジェクトからコピペで貼り付けて下さい。
そして下にスクロールして頂き画像３の赤枠の部分にチェックを入れ右上の「Continue」をクリックして下さい。

[画像1]
![スクリーンショット 2021-07-08 8.15.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/db282b1b-fd07-29cc-b2c5-7425c9e20059.png)

[画像2]
![スクリーンショット 2021-07-08 8.16.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/818e4468-84c5-e4f0-1594-87e12007c028.png)

[画像3]
![スクリーンショット 2021-07-08 8.17.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/12f32dfd-bbf2-bf44-d172-6547ea941a64.png)

下記の画像の画面になりますので右上の「Register」をクリックして登録完了です。
そうすると「Identifiers」に先ほど作成したものが追加されていれば成功です。
これでApple developerにアプリの登録は完了です。
![スクリーンショット 2021-07-08 8.18.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/a5a2320b-733e-b1ea-1ff3-cbd1c5f73c30.png)

#手順2 Firebaseの設定
今回はFirebaseでのプロジェクト作成の方法については割愛させて頂き「Sign in with Apple」の方法だけ説明させて頂きます。
ご自身で作成したプロジェクトを選択して頂き「Authentication」を選択し「Sigan-in method」を選択します。
そこでログイン方法を選択するのですが「Apple」を選択して頂くと画像２の様な画面になりますので無効になっているのを有効に変更し保存して下さい。
これでFirebaseの設定は終わりです。


[画像1]
![スクリーンショット 2021-07-25 9.17.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/fa0dee58-0884-0fd9-eb33-462c5cdb79ef.png)

[画像2]
![スクリーンショット 2021-07-25 9.16.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/70e5b055-8abc-9317-c41a-25f00d7c7b1c.png)

#手順3 Appleログイン機能の実装
今回cocoaPodsに下記の4つをインストールします。

```
  pod 'Firebase'
  pod 'Firebase/Auth'
  pod 'Firebase/Firestore'
  pod 'PKHUD', '~> 5.0'
```
続いてXcodeでプロジェクトを開いて頂き下記の画像の①を選択し②をクリックして下さい。
そして画像２のような画面になりますので③で「Apple」と検索すると④の様に「Sign in with Apple」が出てきますのでそちらをダブルクリックして追加して下さい。
そうすると画像３の赤枠の様に追加されます。


[画像1]
![スクリーンショット 2021-07-25 9.33.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/615458b4-3ad2-a161-ec99-7072c83145b4.png)

[画像2]
![スクリーンショット 2021-07-25 9.43.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/e6365e7d-864f-ca0e-e9f0-ac448c0a5ded.png)

[画像３]
![スクリーンショット 2021-07-25 9.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537088/bfe0beff-ba63-9a0b-6fd5-e9016d35424d.png)


設定は全て終わったのでそれではコードを書いていきます
今回僕はViewControllerに実装しました。

先ずはコードで「Sign in with Apple」ボタンを実装します。
調べた感じだとStoryboardにボタンを配置してやっても上手くいかないらしくコードで実装します。

```ViewController.swift
//先ずは下記をimportして下さい。
import AuthenticationServices
import CryptoKit
import Firebase
import PKHUD
import FirebaseAuth

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if #available(iOS 13.0, *) {
            // ここでインスタンス(ボタン)を生成
            // authorizationButtonTypeでボタンのデザインを変更できる
            // authorizationButtonStyleでボタンの配色を変更できる
            let appleLoginButton = ASAuthorizationAppleIDButton(
                authorizationButtonType: .default,
                authorizationButtonStyle: .whiteOutline
            )
            // ボタン押した時にhandleTappedAppleLoginButtonの関数を呼ぶようにセット
            appleLoginButton.addTarget(
                self,
                action: #selector(handleTappedAppleLoginButton(_:)),
                for: .touchUpInside
            )
            // ↓はレイアウトの設定
            // これを入れないと下の方で設定したAutoLayoutが崩れる
            appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
            // Viewに追加
            view.addSubview(appleLoginButton)
            
            // ↓はAutoLayoutの設定
            // appleLoginButtonの中心を画面の中心にセットする
            appleLoginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            appleLoginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
            // appleLoginButtonの幅は、親ビューの幅の0.7倍
            appleLoginButton.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
            // appleLoginButtonの高さは40
            appleLoginButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
        }
    }

```

続いて上記のコードの下に下記のコードも記述して下さい。
Firebaseのドキュメントによると、最初に実行する必要があるのは、暗号で保護されたナンスを生成し、Appleから認証リクエストを行うときにそれを使用します。
ナンスを生成するコードはFirebaseから提供してくれているのでそれを活用します。

```ViewController.swift

@available(iOS 13.0, *)
    //上記で作成した#selector(handleTappedAppleLoginButton(_:))のobjctの作成をします。
    @objc func handleTappedAppleLoginButton(_ sender: ASAuthorizationAppleIDButton) {
        // ランダムの文字列を生成
        let nonce = randomNonceString()
        // delegateで使用するため代入
        currentNonce = nonce
        // requestを作成
        let request = ASAuthorizationAppleIDProvider().createRequest()
        //これで初回の1回だけemailと名前を取得する事ができます。
        request.requestedScopes = [.email, .fullName]
        // sha256で変換したnonceをrequestのnonceにセット
        request.nonce = sha256(nonce)
        // controllerをインスタンス化する(delegateで使用するcontroller)
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }

    func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            let randoms: [UInt8] = (0 ..< 16).map { _ in
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                return random
            }
            
            randoms.forEach { random in
                if length == 0 {
                    return
                }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        return result
    }
    
    // ⑤SHA256を使用してハッシュ変換する関数を用意
    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
            return String(format: "%02x", $0)
        }.joined()
        
        return hashString
    }
```

下記のコードは認証が成功したときと失敗した時のコードを記述しています。

```ViewController.swift

// ⑥extensionでdelegate関数に追記していく
extension ViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    // 認証が成功した時に呼ばれる関数
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // credentialが存在するかチェック
        guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
            return
        }
        UserDefaults.standard.setValue(appleIDCredential.user, forKey: "appleAuthorizedUserIdKey")
        // nonceがセットされているかチェック
        guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
        }
        // credentialからtokenが取得できるかチェック
        guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
        }
        // tokenのエンコードを失敗
        guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
        }
        // 認証に必要なcredentialをセット
        let credential = OAuthProvider.credential(
            withProviderID: "apple.com",
            idToken: idTokenString,
            rawNonce: nonce
        )
        // Firebaseへのログインを実行
        Auth.auth().signIn(with: credential) { (authResult, error) in
            if let error = error {
                print(error)
                // 必要に応じて
                HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
                return
            }
            if let authResult = authResult {
                print(authResult)
                // 必要に応じて
                HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
                    
                    //ここにログインが完了した時に画面遷移のコードを記述して下さい。
 
                }
            }
        }
    }
    
    // delegateのプロトコルに設定されているため、書いておく
    func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
    
    // Appleのログイン側でエラーがあった時に呼ばれる
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
    }
    
    
}

```
上記のコードで全て実装完了です。
下に全てまとめたコードをを載せておきますので使用する際はコピペで使用して下さい。

```ViewController.swift

//
//  ViewController.swift
//  Sample
//
//
//

import UIKit
import AuthenticationServices
import CryptoKit
import Firebase
import PKHUD
import FirebaseAuth

class ViewController: UIViewController {
    
    fileprivate var currentNonce: String?  
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if #available(iOS 13.0, *) {
            // ここでインスタンス(ボタン)を生成
            let appleLoginButton = ASAuthorizationAppleIDButton(
                authorizationButtonType: .default,
                authorizationButtonStyle: .whiteOutline
            )
            // ボタン押した時にhandleTappedAppleLoginButtonの関数を呼ぶようにセット
            appleLoginButton.addTarget(
                self,
                action: #selector(handleTappedAppleLoginButton(_:)),
                for: .touchUpInside
            )
            // ↓はレイアウトの設定
            // これを入れないと下の方で設定したAutoLayoutが崩れる
            appleLoginButton.translatesAutoresizingMaskIntoConstraints = false
            // Viewに追加
            view.addSubview(appleLoginButton)
            
            // ↓はAutoLayoutの設定
            // appleLoginButtonの中心を画面の中心にセットする
            appleLoginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            appleLoginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
            // appleLoginButtonの幅は、親ビューの幅の0.7倍
            appleLoginButton.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
            // appleLoginButtonの高さは40
            appleLoginButton.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
        }
    }
    
    @available(iOS 13.0, *)
    @objc func handleTappedAppleLoginButton(_ sender: ASAuthorizationAppleIDButton) {
        // ランダムの文字列を生成
        let nonce = randomNonceString()
        // delegateで使用するため代入
        currentNonce = nonce
        // requestを作成
        let request = ASAuthorizationAppleIDProvider().createRequest()
        // sha256で変換したnonceをrequestのnonceにセット
        request.requestedScopes = [.email, .fullName]
        request.nonce = sha256(nonce)
        // controllerをインスタンス化する(delegateで使用するcontroller)
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
    
    func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            let randoms: [UInt8] = (0 ..< 16).map { _ in
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                return random
            }
            
            randoms.forEach { random in
                if length == 0 {
                    return
                }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        return result
    }
    
    // ⑤SHA256を使用してハッシュ変換する関数を用意
    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
            return String(format: "%02x", $0)
        }.joined()
        
        return hashString
    }
}


// ⑥extensionでdelegate関数に追記していく
extension ViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    // 認証が成功した時に呼ばれる関数
    func authorizationController(controller _: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // credentialが存在するかチェック
        guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
            return
        }
        UserDefaults.standard.setValue(appleIDCredential.user, forKey: "appleAuthorizedUserIdKey")
        // nonceがセットされているかチェック
        guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
        }
        // credentialからtokenが取得できるかチェック
        guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
        }
        // tokenのエンコードを失敗
        guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
        }
        // 認証に必要なcredentialをセット
        let credential = OAuthProvider.credential(
            withProviderID: "apple.com",
            idToken: idTokenString,
            rawNonce: nonce
        )
        // Firebaseへのログインを実行
        Auth.auth().signIn(with: credential) { (authResult, error) in
            if let error = error {
                print(error)
                // 必要に応じて
                HUD.flash(.labeledError(title: "予期せぬエラー", subtitle: "再度お試しください。"), delay: 1)
                return
            }
            if let authResult = authResult {
                print(authResult)
                // 必要に応じて
                HUD.flash(.labeledSuccess(title: "ログイン完了", subtitle: nil), onView: self.view, delay: 1) { _ in
                    
                    //ここにログインが完了した時に画面遷移などのコードを記述して下さい。
                }
            }
        }
    }
    
    // delegateのプロトコルに設定されているため、書いておく
    func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
    
    // Appleのログイン側でエラーがあった時に呼ばれる
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
    }
    
    
}

```

#終わり

中々、調べてもこのての記事が出てこず少しでも誰かの手助けになればと思っています。
説明もあまりできない箇所もあるのでもっと自分自身でわかれば随時更新していくつもりです。
ユーザー側に立った時にこのログイン方法を実装すると簡単にログインできるので覚えておくと良いのかなと思っています。
ただ、ユーザ名やプロフィール画像を使用した機能を実装する場合は別で用意して登録してもらう様にしないといけないのでそこは注意が必要です。

最後にもし指摘などがあれば教えていただけると幸いです。