「iCloud CloudKit」と「Sign in with Apple」を使用してユーザー情報を保存します

**ユーザー情報を保存するために第三者のデータベースを使用していますか？iOSプラットフォーム向けだけに開発しているなら、"CloudKit" を "Sign in with Apple" と一緒に使用することを検討してはいかがでしょう。**

> 短時間で77行のコードを使って、ユーザーログイン/登録の機能をiOSアプリに追加できます。

### 何ができるでしょうか？
- ユーザーは、“Sign in with Apple” を使ってあなたのアプリに登録できます。
- 登録後、ユーザーはどのデバイス（同じApple IDでサインインされている物）からでもサインインできて、あなたは氏名、Eメール、その他保存したユーザー情報を取得することが可能です。

# :sunny: 早速始めてみましょう :sunny:

## ステップ1. Xcodeプロジェクト設定で必要な機能をオンにする。“iCloud”の"CloudKit”をオンにする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/aaf99b44-81d2-ce1b-0ca3-91bc9b27414a.png)

「CloudKit」をオンにします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/9a2971f3-b149-49bb-edf3-6ada2127b9b9.png)

既存の"Containers"のトグルボタンをどれかひとつクリックするか、プラスアイコンをクリックして新規"Container"を加える。

### 新規"Container"を加える:

追加ボタンをクリックします

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3633353333302f39613239373166332d623134392d343962622d656466332d3661646132313237623962392e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/891c2b7f-bfac-4a8f-787a-67006722f81e.png)

新しい「Container」の名前を入力してください

<img width="450" alt="Screen Shot 2020-05-12 at 1.06.19 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/bde5384a-e306-91e7-39bf-6b7d124aa054.png">


## ステップ2. ユーザー情報を保存するため、CloudKitのデータ構造を設定する。
- 次に"CloudKit Dashboard"ボタンをクリックするか、http://icloud.developer.apple.com へ行く。
- パネル左側にある自分のアプリケーション名をクリックする。

- "Schema" をクリックします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/6384eeab-c38f-360f-b912-23f9fabe23e9.png)

"New Type" をクリックします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/ab731121-0348-5694-6a66-ae8260e4b93f.png)

タイプに「userInfo」という名前を付けて選択します

右側の[Add field button]ボタンをクリックして、次の新しいフィールドを追加します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/667a2db8-5399-5f60-a399-dd5a880e6f86.png)

「emailAddress」と「name」はどちらも「String」です

<img width="228" alt="Screen Shot 2020-05-12 at 12.54.59 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/e7f479b7-279d-1403-b437-07e486fca166.png">

## ステップ 3. “Sign in with Apple” 機能を追加する (Appleでサインイン)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/18ce3dcf-6ef9-4f2f-7924-b33ec49d973c.png)

“Capabilities” タブは次のようになります:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/e7893b34-0503-4fff-36e5-25c7cee1d674.png)

> :sparkles: **コードはこのページの下部にあります**

## ステップ 4. 新しいloginViewControllerを作成します

```swift
import Foundation
import UIKit
class loginViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
```

### 必要なフレームワークをインポートする

```swift
import AuthenticationServices
import CloudKit
```

###  “Sign in with Apple button”（Appleでサインインのボタン）に、プログラム上の参照を追加しましょう。

```swift
@IBOutlet weak var signInBtnView: UIView!
var authorizationButton: ASAuthorizationAppleIDButton!
```

ここでは、"signInBtnView" という既存ビューにサインインボタンを加えます。ここでは "UIStoryBoard" を使ってAを追加しています。

### このボタンをあなたのプログラムのビューに加えましょう（"UIView"）。

```swift
override func viewDidLoad() {
    super.viewDidLoad()
     authorizationButton = ASAuthorizationAppleIDButton(type: .default, style: .whiteOutline)
    authorizationButton.frame = CGRect(origin: .zero, size: signInBtnView.frame.size)
    authorizationButton.addTarget(self, action: #selector(handleAppleIdRequest), for: .touchUpInside)
    signInBtnView.addSubview(authorizationButton)
}
```

### ボタンがクリックされた際に実行するアクションを処理する

```swift
@objc func handleAppleIdRequest(){
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]
    let authorizationController =     ASAuthorizationController(authorizationRequests: [request])
    authorizationController.delegate = self
    authorizationController.performRequests()
}
```

### 結果やエラーメッセージを受け取るには "delegate"（デリゲート）を設定しましょう。
```swift
extension loginViewController: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    //成功
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
    //失敗した
    print(error.localizedDescription)
}
```

**これで「Sign in with Apple」の運用が開始されたはずです。手元のシミュレータで試して下さい!**

ただし、まだ以下の手続きが必要です: 
1.  サインインでユーザー情報を取得する。
2. 「iCloud」への情報のアップロードと
3. 「iCloud」からの情報の取得。

### サインインからの情報取得

```swift
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as?    ASAuthorizationAppleIDCredential {
        let userID = appleIDCredential.user
        let name = appleIDCredential.fullName?.givenName
        let emailAddr = appleIDCredential.email
    }
}
```

### ここで重要な情報です：

- ユーザーがあなたのアプリで初めて"A"を使った場合、あなたはそのユーザーID、氏名、Eメールアドレスを受け取ります。その情報を保存する"B"については、この記事の後の方で扱います。

- ユーザーが戻ってきた場合（登録ではなくサインインしている場合）、あなたは"userID"だけを受け取ります。"A"についてはこの記事の後の方で扱い、userIDを使って氏名とEメールを取得する方法を学びます。

- ひとりのユーザーの"userID"は、常に同じです。


### ユーザーが登録（新規ユーザー）するのか、サインインするのか、確認しましょう。

```swift
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as?  ASAuthorizationAppleIDCredential {
        let userID = appleIDCredential.user
        if let name = appleIDCredential.fullName?.givenName,
        let emailAddr = appleIDCredential.email {
            //新規ユーザー（登録）
            //この情報を「CloudKit」に保存
        } else {
            //既存ユーザー（サインイン）
            //ユーザー名/メールアドレスの取得
        }
    }
}
```

## ステップ 5. CloudKit (iCloud)

データベースには3種類あることに注意してください。本記事では「privateDatabase（プライベートデータベース）」を使っているので、ユーザーのデータにアクセスできるのはユーザー本人のみとなります。

```swift
//TODO: アプリiCloudコンテナーの名前 (ステップ 1)
let privateDatabase = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").privateCloudDatabase
```

#### 氏名とEメールを受け取った場合（ユーザーが登録しているということ）、新しい"CloudKit"記録を作成できます：

「recordID」は「Appleでサインイン」から取得した「userID」と同じになるように割り当てているため、後でユーザーのCloudKitレコードを取得できます。

```swift
//新規ユーザー（登録）
//この情報を「CloudKit」に保存
let record = CKRecord(recordType: "UserInfo", recordID: CKRecord.ID(recordName: userID))
record["name"] = name
record["emailAddress"] = emailAddr
//保存する他の情報を追加できます
privateDatabase.save(record) { (_, _) in
UserDefaults.standard.set(record.recordID.recordName, forKey: "userProfileID")
```

#### リピートユーザーの場合、"CloudKit" から名前とメールアドレスを取得します:

```swift
//既存ユーザー（サインイン）
//ユーザー名/メールアドレスの取得
privateDatabase.fetch(withRecordID: CKRecord.ID(recordName: userID)) { (record, error) in
    if let fetchedInfo = record {
       let name = fetchedInfo["name"] as? String
        let userEmailAddr = fetchedInfo["emailAddress"] as? String
        //読むべきその他の情報: fetchedInfo[その他の情報] as? String
        //TODO: ユーザー名とメールアドレスを使用できるようになりました（例：ローカルに保存）
        print("名前: \(name) 電子メールアドレス: \(userEmailAddr)")
        UserDefaults.standard.set(name, forKey: "userName")
}
```

完成したコードはこのリンクの先にあります: https://github.com/mszopensource/CloudKitAppleSignInExample/blob/master/loginViewController.swift

```swift
import Foundation
import UIKit
import AuthenticationServices
import CloudKit

class loginViewController: UIViewController {
    
    @IBOutlet var signInBtnView: UIView!
    var authorizationButton: ASAuthorizationAppleIDButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
         authorizationButton = ASAuthorizationAppleIDButton(type: .default, style: .whiteOutline)
        authorizationButton.frame = CGRect(origin: .zero, size: signInBtnView.frame.size)
        authorizationButton.addTarget(self, action: #selector(handleAppleIdRequest), for: .touchUpInside)
        signInBtnView.addSubview(authorizationButton)
    }
    
    @objc func handleAppleIdRequest(){
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.performRequests()
    }
    
}

extension loginViewController: ASAuthorizationControllerDelegate {
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        //TODO: アプリiCloudコンテナーの名前
        let privateDatabase = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").privateCloudDatabase
        if let appleIDCredential = authorization.credential as?  ASAuthorizationAppleIDCredential {
            let userID = appleIDCredential.user
            if let name = appleIDCredential.fullName?.givenName,
                let emailAddr = appleIDCredential.email {
                //新規ユーザー（登録）
                //この情報を「CloudKit」に保存
                let record = CKRecord(recordType: "UserInfo", recordID: CKRecord.ID(recordName: userID))
                record["name"] = name
                record["emailAddress"] = emailAddr
                privateDatabase.save(record) { (_, _) in
                    UserDefaults.standard.set(record.recordID.recordName, forKey: "userProfileID")
                }
            } else {
                //既存ユーザー（サインイン）
                //ユーザー名/メールアドレスの取得
                privateDatabase.fetch(withRecordID: CKRecord.ID(recordName: userID)) { (record, error) in
                    if let fetchedInfo = record {
                        let name = fetchedInfo["name"] as? String
                        let userEmailAddr = fetchedInfo["emailAddress"] as? String
                        
                        //TODO: ユーザー名とメールアドレスを使用できるようになりました（例：ローカルに保存）
                        print("名前: \(name) 電子メールアドレス: \(userEmailAddr)")
                        UserDefaults.standard.set(userID, forKey: "userProfileID")
                    }
                }
            }
        }
    }
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print(error.localizedDescription)
    }
    
}
```

これでうまくいくはずです！

### その他のタイプのユーザー情報の保存
データベースにはその他の情報も保存できます。例えば、ユーザーが好きな動物一覧を保存することができます。

まず、データベースの「スキーム」に「フィールド」を追加する必要があります。[本記事の序盤ですでにその方法を解説しました](https://qiita.com/drafts/bc21527259c5da7f66ed/edit#ステップ1-xcodeプロジェクト設定で必要な機能をオンにするicloudのcloudkitをオンにする)。例として「likedAnimals（好きな動物）」というフィールドを新規追加したいことにしましょう。

<img width="1165" alt="Screen Shot 2020-05-12 at 12.52.43 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/d5d7f0d1-e6ce-9e9e-db4b-bac97c53c28d.png">

<img width="1162" alt="Screen Shot 2020-05-12 at 12.53.35 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/0eafc367-42e5-8b5b-071c-7dadd92a781d.png">

ユーザーのIDをローカルストレージに保存している場合、このコードでユーザーが好きな動物を取得できます。

```swift
func getUserLikedAnimals() {
    let privateDatabase = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").privateCloudDatabase
    if let userCloudID = UserDefaults.standard.string(forKey: "userProfileID") {
        let recordID = CKRecord.ID(recordName: userCloudID)
        privateDatabase.fetch(withRecordID: recordID) { (fetchedRecord, error) in
            if error == nil {
                let likedAnimals = fetchedRecord?.value(forKey: "likedAnimals") as? [String]
                //TODO
            } else {
                print(error?.localizedDescription)
            }
        }
    }
}
```

データベース上のユーザーの記録に動物名を新規追加する場合は、このコードを使用します。

```swift
func updateUserLikedAnimals(newAnimal: String) {
    let privateDatabase = CKContainer(identifier: "iCloud.com.[アプリiCloudコンテナーの名前]").privateCloudDatabase
    if let userCloudID = UserDefaults.standard.string(forKey: "userProfileID") {
        let recordID = CKRecord.ID(recordName: userCloudID)
        privateDatabase.fetch(withRecordID: recordID) { (record, error) in
            guard let fetchedRecord = record else { return }
            if error == nil {
                var likedAnimals = fetchedRecord.value(forKey: "likedAnimals") as? [String] ?? []
                likedAnimals.append(newAnimal)
                //更新記録
                privateDatabase.save(fetchedRecord) { (modifiedRecord, error) in
                    if error != nil {
                        //失敗
                        print(error?.localizedDescription)
                    } else {
                        //成功
                    }
                }
            } else {
                print(error?.localizedDescription)
            }
        }
    }
}
```


***最後にはiCloudの開発用データベースを本番環境にデプロイする必要があります。***

<img width="1013" alt="Screen Shot 2020-05-11 at 4.32.00 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/82288f3a-2f0f-23cd-e1ba-c8ca51002883.png">

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">