[レポート] MOB308: AWSを活用してクラウドに繋がったiOSゲームアプリを作る #reinvent
Build a Cloud-Connected iOS Game with AWS
re:Invent 2018にて、AWS Amplifyを使ってiOSで動作するゲームアプリを作るワークショップに参加してきました。以下が概要です。
As an iOS developer, you understand the frameworks like UIKit and ARKit. But how can you make your app more successful? AWS Mobile has some easy-to-use yet powerful cloud offerings that can help take your app to the next level. In this workshop, we extend an open-source iOS game with social authentication and leaderboards, and we gather analytics that you can use to engage users and enhance their experience.
本記事では、ワークショップで行った内容をレポートします。
Agenda
- Game details
- Toolchain
- Architecture
- Workshop
- Takeaways
Game details
オープンソースゲームのFlappy BirdであるFlappySwiftをforkしたリポジトリでゲームを作ります。
クラウドを利用して、以下の機能を作ります。
- ユーザー登録、ログイン
- ハイスコア
- ユーザー解析
Toolchain
- AWS Amplify CLI(https://bit.ly/AmplifyCLI)
- ユーザー登録、ログイン
- AWS CloudFormation
- ハイスコアのAPIを構築
- Xcode (9.2+)
- CocoaPods
Architecture
- Amazon Cognitoでユーザー登録、ログイン
- AWS AppSyncでAmazon DynamoDBにハイスコアデータを保存
- Amazon Pinpointでユーザー行動ログを取得
Workshop
以下のリポジトリをcloneして行います。
まずは起動する
まずはcloneしてXcodeで起動するところまで試します。
$ git clone [email protected]:rohandubal/FlappySwift.git $ cd FlappySwift $ open FlappyBird.xcodeproj
Xcodeが起動したら Cmd
+ R
で起動。
おなじみのFlappy Bird!
Amplifyでサービスを立ち上げる
まずはAmplifyでCloudFormation実行用のIAM Userを作成します。
$ amplify configure
次に amplify init
で初期化(IAM RoleやS3バケットの作成)を行います。
$ amplify init Note: It is recommended to run this command from the root of your app directory ? Choose your default editor: Atom Editor ? Choose the type of app that you're building ios Using default provider awscloudformation For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html ? Do you want to use an AWS profile? Yes ? Please choose the profile you want to use workshop ⠏ Initializing project in the cloud... CREATE_IN_PROGRESS lappywift-20181127085924 AWS::CloudFormation::Stack Tue Nov 27 2018 08:59:27 GMT-0800 (PST) User Initiated CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Tue Nov 27 2018 08:59:32 GMT-0800 (PST) CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Tue Nov 27 2018 08:59:32 GMT-0800 (PST) CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Tue Nov 27 2018 08:59:32 GMT-0800 (PST) CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Tue Nov 27 2018 08:59:32 GMT-0800 (PST) Resource creation Initiated CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Tue Nov 27 2018 08:59:32 GMT-0800 (PST) Resource creation Initiated CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Tue Nov 27 2018 08:59:33 GMT-0800 (PST) Resource creation Initiated ⠏ Initializing project in the cloud... CREATE_COMPLETE AuthRole AWS::IAM::Role Tue Nov 27 2018 08:59:44 GMT-0800 (PST) CREATE_COMPLETE UnauthRole AWS::IAM::Role Tue Nov 27 2018 08:59:46 GMT-0800 (PST) CREATE_COMPLETE DeploymentBucket AWS::S3::Bucket Tue Nov 27 2018 08:59:53 GMT-0800 (PST) CREATE_COMPLETE lappywift-20181127085924 AWS::CloudFormation::Stack Tue Nov 27 2018 08:59:56 GMT-0800 (PST) ✔ Successfully created initial AWS cloud resources for deployments. Your project has been successfully initialized and connected to the cloud! Some next steps: "amplify status" will show you what you've added already and if it's locally configured or deployed "amplify <category> add" will allow you to add features like user login or a backend API "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud Pro tip: Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything
CocoaPodsで各ライブラリをインストールする
CocoaPodsで各ライブラリをインストールします。
$ pod init $ vim Podfile
Podfile
を以下のようにします。
# Uncomment the next line to define a global platform for your project platform :ios, '9.0' target 'FlappyBird' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for FlappyBird ####### AWS Frameworks - Begin Copy ###### pod 'AWSMobileClient', '~> 2.7.0' pod 'AWSAuthUI', '~> 2.7.0' pod 'AWSUserPoolsSignIn', '~> 2.7.0' pod 'AWSAppSync', '~> 2.6.25' pod 'AWSPinpoint', '~> 2.7.0' ####### AWS Frameworks - End Copy ######## target 'FlappyBirdTests' do inherit! :search_paths # Pods for testing end end
あとは pod install
で上記に記載したライブラリをインストールします。
$ pod install --repo-update CocoaPods 1.6.0.beta.2 is available. To update use: `gem install cocoapods --pre` [!] This is a test version we'd love you to try. For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.6.0.beta.2 Analyzing dependencies Downloading dependencies Installing AWSAppSync (2.6.25) Installing AWSAuthCore (2.7.3) Installing AWSAuthUI (2.7.3) Installing AWSCognitoIdentityProvider (2.7.3) Installing AWSCognitoIdentityProviderASF (1.0.1) Installing AWSCore (2.7.3) Installing AWSMobileClient (2.7.3) Installing AWSPinpoint (2.7.3) Installing AWSUserPoolsSignIn (2.7.3) Installing ReachabilitySwift (4.0.0) Installing SQLite.swift (0.11.4) Generating Pods project Integrating client project [!] Please close any current Xcode sessions and use `FlappyBird.xcworkspace` for this project from now on. Sending stats Pod installation complete! There are 5 dependencies from the Podfile and 11 total pods installed.
Amplifyでクラウドの機能を追加する
Amplify CLI経由で、AWSのリソースを準備していきます。
ユーザー認証
$ amplify add auth
こちらを実行すると設定について問われるので default configuration
を選びます。
$ amplify push
Xcodeを開きます。
$ open FlappyBird.xcworkspace
設定が記載された awsconfiguration.json
ファイルを FlappyBird
グループの中にドラッグ&ドロップで追加します。
AppDelegate.swift
の中身を以下に変えます。
import UIKit import AWSMobileClient @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { self.initializeAWSMobileClient() return true } func initializeAWSMobileClient() { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { print("UserState: \(userState.rawValue)") } else if let error = error { print("error: \(error.localizedDescription)") } } } }
次に MainViewController.swift
を以下に変えます。showSignInScreenIfNotLoggedIn()
メソッドが主に追加した実装です。
import Foundation import UIKit import AWSMobileClient class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.tintColor = UIColor.white self.navigationController?.navigationBar.barTintColor = UIColor.purple self.showSignInScreenIfNotLoggedIn() } func showSignInScreenIfNotLoggedIn() { // If user is not signed in, we show the sign in screen. if(!AWSMobileClient.sharedInstance().isSignedIn) { AWSMobileClient.sharedInstance().showSignIn(navigationController: self.navigationController!) { (userState, error) in if (error == nil) { print("User State is \(userState!.rawValue)") } } } else { // User is already logged in. } } @IBAction func onHighScoresClicked(_ sender: Any) { } @IBAction func onPlayClicked(_ sender: Any) { } @IBAction func onSignOutClicked(_ sender: Any) { AWSMobileClient.sharedInstance().signOut() self.showSignInScreenIfNotLoggedIn() } }
これで動かすとログイン機能が使えるようになっています。
行動履歴
$ amplify add analytics $ amplify push
AppDelegate.swift
にPinpointの初期化が行われるようにコードを修正します。
import UIKit import AWSMobileClient // ここを追加 import AWSPinpoint @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // ここを追加 static var pinpoint: AWSPinpoint? func initializeAWSMobileClient() { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { print("UserState: \(userState.rawValue)") // ここを追加 AppDelegate.pinpoint = AWSPinpoint(configuration: AWSPinpointConfiguration.defaultPinpointConfiguration(launchOptions: nil)) } else if let error = error { print("error: \(error.localizedDescription)") } } } .....
MainViewController.swift
も修正します。extentionでメソッドを追加。
// Mark: AWS Analytics Extension extension MainViewController { // This function logs an event when the user clicks on `High Scores` button func logShowHighScore() { let event = AppDelegate.pinpoint?.analyticsClient.createEvent(withEventType: "ShowHighScores") AppDelegate.pinpoint?.analyticsClient.record(event!) } // This function logs an event when the user clicks on `Play` button func logPlayGame() { let event = AppDelegate.pinpoint?.analyticsClient.createEvent(withEventType: "PlayGame") AppDelegate.pinpoint?.analyticsClient.record(event!) } // This function logs an event when the user sees the main screen of app and records if they are a new user or returning user func logReturningOrNewUser() { let event = AppDelegate.pinpoint?.analyticsClient.createEvent(withEventType: "AppLaunch") if (AWSMobileClient.sharedInstance().isSignedIn) { event?.addAttribute("NewLogin", forKey: "EntryMechanism") } else { event?.addAttribute("PreviousLogin", forKey: "EntryMechanism") } AppDelegate.pinpoint?.analyticsClient.record(event!) } }
既存のメソッドにログ送信処理を追加します。
override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.tintColor = UIColor.white self.navigationController?.navigationBar.barTintColor = UIColor.purple self.showSignInScreenIfNotLoggedIn() // ここを追加 self.logReturningOrNewUser() } @IBAction func onHighScoresClicked(_ sender: Any) { // ここを追加 self.logShowHighScore() } @IBAction func onPlayClicked(_ sender: Any) { // ここを追加 self.logPlayGame() } @IBAction func onSignOutClicked(_ sender: Any) { AWSMobileClient.sharedInstance().signOut() self.showSignInScreenIfNotLoggedIn() }
ハイスコアの記録
AppSync APIを作るためにCloudFormation Stackを新規作成します。
- CloudFormationのコンソールからスタックを新規作成
- リポジトリ内の
ScoresGraphQLAPI.yaml
をテンプレートに使う - パラメータを設定する
graphQlApiName
:FlappyBirdScoreAPI
userPoolId
: 以下のコマンドで抽出userPoolAwsRegion
:userPoolId
の前方
userPoolId
は設定ファイルより以下のコマンドで抽出します。
$ grep -s "UserPoolId" amplify/backend/amplify-meta.json | awk '{print $2}'
Stackが起動できたらAppSync Consoleを開いてAPIのIDをコピーし、以下のコマンドを実行します。
$ amplify add codegen --apiId <API Id Here>
出来上がった API.swift
はXcodeプロジェクト内に追加します。
AppDelegate.swift
を修正します。
import UIKit import AWSMobileClient import AWSPinpoint // ここを追加 import AWSAppSync @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? static var pinpoint: AWSPinpoint? // ここを追加 static var appSyncClient: AWSAppSyncClient? func initializeAWSMobileClient() { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { print("UserState: \(userState.rawValue)") AppDelegate.pinpoint = AWSPinpoint(configuration: AWSPinpointConfiguration.defaultPinpointConfiguration(launchOptions: nil)) // ここを追加 self.initializeAppSync() } else if let error = error { print("error: \(error.localizedDescription)") } } } // ここを追加 func initializeAppSync() { // You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("game_scores") 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 AppDelegate.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") } } .....
次に GameViewController.swift
にExtensionを追加します。
// Mark: AWS Cloud Functions extension GameViewController { func logGameFinishedEvent(score: Int) { let event = AppDelegate.pinpoint?.analyticsClient.createEvent(withEventType: "GamePlayed") event?.addMetric(score as NSNumber, forKey: "Score") AppDelegate.pinpoint?.analyticsClient.record(event!) } func submitScore(score: Int) { let newScore = CreateScoreInput(score: score, scoreDate: Int(Date().timeIntervalSince1970)) let newScoreMutation = CreateScoreMutation(input: newScore) AppDelegate.appSyncClient?.perform(mutation: newScoreMutation) } }
また、予め用意されている GameEnded
プロトコルを実装しているExtensionを以下に修正します。
// Mark: Game ended actions extension GameViewController: GameEnded { func gameEnded(score: Int) { showPopupWithOptions(title: "Game Finished!", message: "You scored \(score.description)! \nWhat do you want to do next?") /******* Begin copy code *********/ self.logGameFinishedEvent(score: score) self.submitScore(score: score) /******* End copy code *********/ } }
最後に ScoresViewController.swift
を修正します。まず以下のメソッドを追加します。
func loadTopScores() { let query = ListTopScoresQuery() // fetch from the cache first for a responsive UI AppDelegate.appSyncClient?.fetch(query: query, cachePolicy: .returnCacheDataAndFetch, resultHandler: { (result, error) in if let result = result { var localScores = [String]() guard result.data?.listTopScores != nil else { return } if result.data!.listTopScores!.items!.count > 0 { for item in result.data!.listTopScores!.items! { localScores.append("\(item!.score) \t\t \(item!.username)") } self.scores = localScores } } }) }
このメソッドを viewDidLoad()
で呼び出します。
override func viewDidLoad() { scoresTableView.delegate = self scoresTableView.dataSource = self self.scoresTableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier) // Call loadTopScores to show the leader board self.loadTopScores() }
以上で全ての実装が終わりです!
動かしてみる
Xcodeで Cmd
+ R
で実行してみます。そして普通にFlappy Birdを楽しみます。
ハイスコアを見てみると記録されていることが確認できます。
まとめ
バックエンドを作りながらiOSを実装するのはなかなか大変ですが、Amplify CLIによってクラウド側のリソースがコマンド一発で用意できるのはかなり使い勝手が良いです。
しかしながら完全にAmplify CLIの機能だけで全てのAWSリソースが用意できるわけにはいかず、あくまで主要なサービスのリソースだけ作れるという形になっています。それ以外はやはりCloudFormationを直接使うというパターンになります。
また、AppSyncのcodegenについては以前はちょっと煩雑でしたが、こちらもAmplify CLIに含まれているので開発環境依存が減るなど、より効率的になっており好感が持てました。今後もデベロッパーにとってより使いやすくなるようなアップデートを期待しています!