AWS AmplifyによるiOSアプリ開発入門（Part3）

# 概要

iOSアプリからAppSyncをつかって、GraphQLでDynamoDBにデータを格納する部分を実装します。
また、認証部分はCognitoを使い、AppSyncの構築はAWS Amplifyを使用します。


# 前提条件

[AWS AmplifyによるiOSアプリ開発入門（Part1）](https://qiita.com/papi_tokei/items/55d48a1a9a9e56d7cbd4)と[AWS AmplifyによるiOSアプリ開発入門（Part2）](https://qiita.com/papi_tokei/items/f984abdef1518ae8918b)が完了していること
Part1で作成したXcodeプロジェクトをそのまま使用します

# AWS Amplifyを使ってAppSyncを構築

```shell
# Xcodeのルートプロジェクトに移動
$ cd ~/Downloads/amplify-learn

# AppSyncを作成（実際に作成されるのは「amplify push」後）
$ amplify add api
# Please select from one of the below mentioned services
# →GraphQLを選択
# Provide API name
# →amplifylearnを選択（名前は任意）
# Choose an authorization type for the API
# →Amazon Cognito User Poolを選択
# Do you have an annotated GraphQL schema?
# →Noを選択
# Do you want a guided schema creation?
# yesを選択
# What best describes your project
# →Single object with fields (e.g., “Todo” with ID, name,description)を選択
# Do you want to edit the schema now? 
# →Noを選択

# AppSyncを作成するようにクラウド側に命令
$ amplify push
# Are you sure you want to continue?
# →yを選択
# Do you want to generate code for your newly created GraphQL API
# →yを選択
# Enter the file name pattern of graphql queries, mutations and subscriptions(graphql/**/*.graphql)
# →デフォルトのままEnterキーを押す
# Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions
# →yを選択
# Enter the file name for the generated code (API.swift)
# →デフォルトのままEnterキーを押す
```

上記コマンドの内容をざっくりと解説します。
今回はGraphQLを使用してクラウド側とデータのやり取りを行います。
GraphQLの詳細な説明は今回は割愛させて頂きます。

まず、`$ amplify add api`コマンドを実行した時点で、「~/Downloads/amplify-learn/amplify/backend/api/amplifylearn/schema.graphql」にGraphQLのスキーマ定義が保存されます。

```:scheme.graphql
type Todo @model {
  id: ID!
  name: String!
  description: String
}
```
`@modelディレクティブ`は、DynamoDBにデータを保存する、という意味になります。
なので、上記のスキーマ定義をするだけで、amplify push後に自動的にDynamoDBのテーブルが作成されます。
（ディレクティブの説明は[ここ](https://aws-amplify.github.io/docs/js/api)のUsing GraphQL Transformersを参照）

次に`$ amplify push`を実行することで、クラウドフォーメーションが起動して、AppSyncを作成します。
いくつか質問されますが、重要なのは「Do you want to generate code for your newly created GraphQL API」の質問です。
この質問に対してyesで答えることで、iOS側でAppSyncで使用する関数を自動的に作成してくれます。
まず、graphqlディレクトリ以下にミューテーション、クエリ、サブスクリプションの定義ファイルが作成されます。

<img width="1128" alt="mutations_graphql_—_amplify-learn.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/24ed210f-ff18-26c9-3639-a88d28767049.png">

このファイルに基づいて、swift側のコードからミューテーション、クエリ、サブスクリプションを実行するためのコードが、API.swiftに自動的に作成・保存され、簡単にgraphQLを実行することが可能になります。

また、awsconfig.jsonにAppSyncの項目が追加されており、GraphQLのエンドポイントが記載されていることが分かります。

```json:awsconfig.json(抜粋)
"AppSync": {
        "Default": {
            "ApiUrl": "https://vf5rq526ubhxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql",
            "Region": "ap-northeast-1",
            "AuthMode": "AMAZON_COGNITO_USER_POOLS"
        }
    }
```

# AppSync経由でDynamoDBにデータを格納する部分の実装

## 使用するiOS SDKのインストール

Podfileを以下のように編集して下さい。

```:Podfile
target 'amplify-learn' do
  use_frameworks!

  # Pods for amplify-learn
  pod 'AWSMobileClient', '~> 2.7.0'
  pod 'AWSAuthUI', '~> 2.7.0'
  pod 'AWSUserPoolsSignIn', '~> 2.7.0' 
  pod 'AWSS3', '~> 2.7.0'
  pod 'AWSAppSync', ' ~> 2.6.24' # AppSync用のSDKを追加
end
```

```shell
# Xcodeプロジェクトのルートに移動
$ cd ~/Downloads/amplify-learn

# 必要なSDKを追加
$ pod install --repo-update
```

最後にAPI.swiftをXcodeプロジェクトに含めるようにします。

* **amplify-learn.xcworkspace**を開く
* プロジェクト直下にAPI.swiftをドラッグ&ドロップする
* Copy items if neededのチェックを外し、Create groupsを選択し、「Finish」をクリックする

## AppSyncの設定を行う

AppDelegeta.swiftに以下のコードを追記します。
AppSyncの初期設定を行います。
今回はCognitoユーザープールを使用するようにしています。
※以下のコードは抜粋ですので、記載のない行は削除しないようにお願いします。

```swift:AppDelegate.swift(抜粋)

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
	let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("database_name")
        
    do {
        // Initialize the AWS AppSync configuration
        let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(),
          userPoolsAuthProvider: {
            class MyCognitoUserPoolsAuthProvider : AWSCognitoUserPoolsAuthProviderAsync {
                func getLatestAuthToken(_ callback: @escaping (String?, Error?) -> Void) {
                    AWSMobileClient.sharedInstance().getTokens { (tokens, error) in
                        if error != nil {
                            callback(nil, error)
                        } else {
                            callback(tokens?.idToken?.tokenString, nil)
                        }
                    }
                }
            }
            return MyCognitoUserPoolsAuthProvider()}(),
          databaseURL:databaseURL)
        
        // Initialize the AWS AppSync client
        appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig)
    } catch {
        print("Error initializing appsync client. \(error)")
    }
}
```

次にViewController.swiftにもAppSyncの設定を追加します。
※以下のコードも抜粋ですので、記載の無い行は削除しないようにお願いします。

```swift:ViewController.swift(抜粋)

import AWSAppSync

class ViewController: UIViewController {
	var appSyncClient: AWSAppSyncClient?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        appSyncClient = appDelegate.appSyncClient
     }
}
```

## DynamoDBにレコードを追加する部分を実装

では、実際にDynamoDBにレコードを追加する部分の実装を行います。
ViewController.swiftに以下の関数を追加して下さい。

```swift:ViewController.swift(抜粋)

func runMutation(){
        let mutationInput = CreateTodoInput(name: "Use AppSync", description:"Realtime and Offline")
        appSyncClient?.perform(mutation: CreateTodoMutation(input: mutationInput)) { (result, error) in
            if let error = error as? AWSAppSyncClientError {
                print("Error occurred: \(error.localizedDescription )")
            }
            if let resultError = result?.errors {
                print("Error saving the item on server: \(resultError)")
                return
            }
        }
    }
```

上記のコードを簡単に解説します。
`CreateTodoInput(name: "Use AppSync", description:"Realtime and Offline")`
`CreateTodoInput`は、API.swift内に定義されている関数で、自動的に作成された構造体です。
この関数を使うことで、入力パラメータを作成することが可能です。
今回は「name: "Use AppSync", description:"Realtime and Offline"」というTODOを作成することにします。

`appSyncClient?.perform(mutation: CreateTodoMutation(input: mutationInput)) { (result, error) in`
こちらも同じく、`CreateTodoMutation`はAPI.swiftに定義されているクラスです。
`appSyncClient?.perform`を実行すると、AppSyncのcreateTodoに設定されているresolverが実行され、DynamoDBにデータが追加されます。（下図参照）

<img width="1363" alt="AWS_AppSync_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/f3842fb4-60f7-1558-5757-9f0379e96070.png">

![AWS_AppSync_Console.png](https://qiita-image-store.s3.amazonaws.com/0/192873/05c0d6ab-3f0e-5adc-fc77-207fc2e86a1b.png)

では、ボタンを配置してrunMutation関数を実行するように設定しましょう。
Main.storyboardに「Dynamoにデータを追加」ボタンを追加し、ViewController.swiftに対してアクション接続を追加してください
connectionはAction、NameをpushDataToDynamoとして設定します
関数は以下のようになるかと思います。

```swift:ViewController.swift(抜粋)
@IBAction func pushDataToDynamo(_ sender: Any) {
    runMutation()
}
```

「Dynamoにデータを追加」ボタンをクリックすると、DynamoDBにデータが追加されることが分かると思います。
画面は下図のような感じです。

<img width="423" alt="iPhone_XR_-_12_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/9aef6fd4-e6b7-2ba2-39ef-3b18415aefb0.png">

## DynamoDBからデータを取得する部分の実装

Dynamoにデータを追加する手順と同様に、ボタンの追加、コードの修正を行います。
Main.storyboardに「Dynamoからデータを取得」ボタンを追加し、ViewController.swiftに対してアクション接続を追加してください
connectionはAction、NameをgetDynamoDataとして設定します
また、runQuery関数も追加して下さい。

```swift:ViewController.swift(抜粋)
@IBAction func getDynamoData(_ sender: Any) {
    runQuery()
}

func runQuery(){
    appSyncClient?.fetch(query: ListTodosQuery()) {(result, error) in
        if error != nil {
            print(error?.localizedDescription ?? "")
            return
        }
        result?.data?.listTodos?.items!.forEach { print(($0?.name)! + " " + ($0?.description)!) }
    }
}
```

runQuery関数について、簡単に説明します。
runMutation関数を同じような構造ですが、`result?.data?.listTodos?.items!.forEach { print(($0?.name)! + " " + ($0?.description)!) }`の部分で、取得したレコードのnameとdescriptionをコンソールに表示する処理を記述しています。

エミュレーターを起動して、「Dynamoからデータを取得」ボタンををクリックすると、コンソールにデータが表示されることが分かると思います。（下図参照）

<img width="606" alt="ViewController_swift.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/f93eb8b7-c5cd-8e73-accd-2c46f65de628.png">


ちなみに、参照元のDynamoDBのテーブルは以下です。
![DynamoDB_·_AWS_Console.png](https://qiita-image-store.s3.amazonaws.com/0/192873/620f1de1-29ce-7369-9f15-6e208fc7e0f2.png)

最終的な画面はこんな感じです。
<img width="423" alt="iPhone_XR_-_12_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/7cfe9489-bfe0-1bba-fce9-cb1040217938.png">

## DynamoDBにレコード追加をサブスクライブする部分の実装

DynamoDBにデータが追加されたタイミングで、そのデータを受け取れるようなサブスクライブ機能を実装します。
データ全てではなく、追加されたデータのみを受け取ることができるので、アップデートの情報などを配信する機能の実装などで利用できそうです。

これまでと同様に、ボタンの追加、コードの修正を行います。
Main.storyboardに「サブスクライブ開始」ボタンを追加し、ViewController.swiftに対してアクション接続を追加してください
connectionはAction、NameをstartSubscribeとして設定します
また、subscribe関数も追加して下さい。

```swift:ViewController.swift
@IBAction func startSubscribe(_ sender: Any) {
    subscribe()
}

var discard: Cancellable?
func subscribe() {
    do {
        discard = try appSyncClient?.subscribe(subscription: OnCreateTodoSubscription(), resultHandler: { (result, transaction, error) in
            if let result = result {
                print(result.data!.onCreateTodo!.name + " " + result.data!.onCreateTodo!.description!)
            } else if let error = error {
                print(error.localizedDescription)
            }
        })
    } catch {
        print("Error starting subscription.")
    }
}
```

サブスクライブの関数もこれまでの関数と同じ構造ですが、サブスクライブはバックグラウンドで動作し続ける点が異なります。

エミュレータを起動して、「サブスクライブ開始」ボタンをクリックして、「Dynamoにデータを追加」ボタンをクリックすると、追加したデータがコンソールにリアルタイム（少し遅延はありますが）に表示されることが分かります。
最終的な画面は以下のようになります。
<img width="423" alt="iPhone_XR_-_12_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/192873/3d79710c-0a3c-22cc-c23a-99cb875c24ec.png">

# まとめ

今回はAppSyncを用いて、DynamoDBに対して操作する部分を説明しました。
ですが、AppSyncの詳細な説明、GraphQLの詳細な説明は省略しましたので、全体的にはわかりにくい記事になっているかと思います。
AppSyncについてと、GraphQLについては、私もしっかり理解できていませんので、別記事で詳細に解説したいと思います。

AWS AmplifyによるiOSアプリ開発入門は、本記事で終了となります。
読んで頂きありがとうございます。