Xcodeからバックエンドを作る!Amplify iOSを試してみた #reinvent

re:Invent 2019の期間中に公開された「Amplify iOS」では、iOS SDKとしての機能アップデートだけではなくXcodeからAPIが構築できるなど新しい仕組みが導入されています。早速試してみました!
2019.12.19

Amplify iOS

Amplify iOSおよびAmplify Androidは、AWS re:Invent 2019で発表されたAmplify Frameworkの新しいツール・ライブラリです。現在はプレビュー版として提供されています。

本記事ではGetting Startedに沿ってiOSアプリを作ってみました。

サンプルコードをGitHubで公開しましたので、合わせてご確認いただけますと幸いです。

インストール

Xcodeプロジェクト、およびCocoaPodsはすでに作成・導入済みの前提で進みます。本記事では AmplifySample という名前のXcodeプロジェクトを作成しました。

まずはXcodeプロジェクトのディレクトリを開き Podfile を作成します。

$ cd ./AmplifySample
$ pod init

Podfile は以下のようにします。

Podfile

target 'AmplifySample' do
  use_frameworks!

  # Pods for AmplifySample
  pod 'amplify-tools'
  pod 'Amplify'
  pod 'AWSPluginsCore'
  pod 'AmplifyPlugins/AWSAPIPlugin'
end

pod install を実行し、各ライブラリをインストールします。

$ pod install --repo-update

Xcodeワークスペースが作成されます AmplifySample.xcworkspace を開きビルドを行います。ビルドは Command + B で行えます。

ビルドすると下図のように3つの設定ファイルが生成されます。

それぞれ次のような設定が含まれるようになります。

  • awsconfiguration.jsonamplifyconfiguration.json : AmplifyとしてAWSの各リソースを作る際の設定ファイル
  • amplifyxc.config : XcodeからAmplify Frameworkの操作を行う際の設定ファイル

モデルの作成

Amplify iOSでは、デフォルトでGraphQL (AppSync)と通信することでデータを取得したりする前提で各設定が行われています。

amplify/backend/api/amplifyDatasource/schema.graphql にファイルが作成されており、確認してみると下記のようなGraphQLスキーマが作られています。

type Task @model {
  id: ID!
  title: String!
  description: String
  status: String
}
type Note @model {
  id: ID!
  content: String!
}

このGraphQL Schemaを元に、モデルクラスを作ってみます。モデルクラスを作る場合は amplifyxc.config の設定を変更し modelgen=true にします。

push=false
modelgen=true
profile=default
envName=amplify

ビルドすると次のファイルが amplify/generated/models に生成されます。

  • AmplifyModels.swift
  • Note.swift
  • Note+Schema.swift
  • Task.swift
  • Task+Schema.swift

生成したファイルはXcode ProjectのTarget内に追加します。

最後にビルドしておきます。

APIとDatabaseの追加

push=true
modelgen=true
profile=default
envName=amplify

profile~/.aws/config に設定してあるProfileを利用することが可能です。しかしMFAには対応していないようなのでご留意ください。MFAが不要なIAM Userで実行可能です。

ビルドを実行するとAWSアカウント上に各種リソースが作成されます。awsconfiguration.jsonamplifyconfiguration.json に設定が増えていることが確認できます。

awsconfiguration.json

{
    "UserAgent": "aws-amplify/cli",
    "Version": "0.1.0",
    "IdentityManager": {
        "Default": {}
    },
    "AppSync": {
        "Default": {
            "ApiUrl": "https://<api id>.appsync-api.ap-northeast-1.amazonaws.com/graphql",
            "Region": "ap-northeast-1",
            "AuthMode": "API_KEY",
            "ApiKey": "<api key>",
            "ClientDatabasePrefix": "amplifyDatasource_API_KEY"
        }
    }
}

amplifyconfiguration.json

{
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "amplifyDatasource": {
                    "endpointType": "GraphQL",
                    "endpoint": "https://<api id>.appsync-api.ap-northeast-1.amazonaws.com/graphql",
                    "region": "ap-northeast-1",
                    "authorizationType": "API_KEY",
                    "apiKey": "<api key>"
                }
            }
        }
    }
}

APIを利用したアプリの実装

ここまででAPIの用意ができたので、アプリにAPIを呼び出す処理を実装します。

AppDelegate での初期化

AppDelegate.swiftapplication:didFinishLaunchingWithOptions: メソッド内に次の処理を追加します。

AppDelegate.swift

import UIKit
import Amplify
import AmplifyPlugins

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let apiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels())
        do {
            try Amplify.add(plugin: apiPlugin)
            try Amplify.configure()
            print("Amplify initialized")
        } catch {
            print("Failed to configure Amplify \(error)")
        }
        return true
    }
}

View Controllerで書き込み/読み込みを試す

次にView Controller内でAPIに対してのデータの書き込み/読み込みを試していきたいと思います。

まずView Controllerでデータの確認ができるよう、下図のようにViewを配置します。

続いてView Controllerの実装です。以下のように実装しました。

ViewController.swift

import UIKit
import Amplify

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var tableView: UITableView!
    var list = [Note]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        query()
        subscribe()
    }
    
    @IBAction func postButtonDidTouch(_ sender: Any) {
        let content = textField.text ?? ""
        let note = Note(content: content)
        _ = Amplify.API.mutate(of: note, type: .create) { (event) in
            switch event {
            case .completed(let result):
                switch result {
                case .success(let note):
                    print("API Mutate successful, created note: \(note)")
                    DispatchQueue.main.async {
                        self.textField.text = nil
                    }
                case .failure(let error):
                    print("Completed with error: \(error.errorDescription)")
                }
            case .failed(let error):
                print("Failed with error \(error.errorDescription)")
            default:
                print("Unexpected event")
            }
        }
    }
    
    func query() {
        _ = Amplify.API.query(from: Note.self, where: nil) { (event) in
            switch event {
            case .completed(let result):
                switch result {
                case .success(let notes):
                    print("Successfully retrieved list of notes: \(notes)")
                    self.list = notes
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                    }
                case .failure(let error):
                    print("Got failed result with \(error.errorDescription)")
                }
            case .failed(let error):
                print("Got failed event with error \(error)")
            default:
                print("Should never happen")
            }
        }
    }
    
    func subscribe() {
        _ = Amplify.API.subscribe(from: Note.self, type: .onCreate) { (event) in
            switch event {
            case .inProcess(let subscriptionEvent):
                switch subscriptionEvent {
                case .connection(let subscriptionConnectionState):
                    print("Subsription connect state is \(subscriptionConnectionState)")
                case .data(let result):
                    switch result {
                    case .success(let note):
                        print("Successfully got todo from subscription: \(note)")
                        self.list.append(note)
                        DispatchQueue.main.async {
                            self.tableView.reloadData()
                        }
                    case .failure(let error):
                        print("Got failed result with \(error.errorDescription)")
                    }
                }
            case .completed:
                print("Subscription has been closed")
            case .failed(let error):
                print("Got failed result with \(error.errorDescription)")
            default:
                print("Should never happen")
            }
        }
    }

}

extension ViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell")
        let note = list[indexPath.row]
        cell.textLabel?.text = note.content
        return cell
    }
    
}

上記の実装コードのポイントは下記の通りです。

  • View Controllerが表示された際にSubscribeを開始
  • TextInputに入力し、Buttonで投稿をしたらAPIにリクエスト
  • 新しいデータが作成されたらSubscribeイベントが到着
  • 新しいデータをTableViewに反映

以上で完了です。

動かす

それではiOSアプリとして動かしてみます。Command + R でデバッグ起動します。何かしらを書き込んでみます。投稿するとクラウド上にデータが保存され、TableViewにアイテムが追加されます。

バックエンド側を簡単に確認してみます。CloudFormationを見てもらうと分かりますが、Amplify経由でAppSyncやDynamoDB、Cognito User Poolといったリソースが作成されています。

DynamoDBのNoteテーブルを確認してみると、先ほど追加したデータが格納されていることが分かります。

コマンドラインでの操作不要でAmplify Frameworkが使える!

Amplify FrameworkではAmplify CLIをコマンドラインで実行しなければAPIの作成などは行えませんでした。今回のAmplify iOSおよびAmplify AndroidではXcodeなどの統合開発環境(IDE)からAmplify Frameworkの操作が行えます。これはモバイルアプリ開発者にとっては非常に嬉しい限りですね!

他にもPredictionsなどの面白い機能があるので、試していきたいと思います。