- Xcode 9.2+
- Cocoapods
- Node.js
- amplify-cli
- First, clone the project using the command line
git clone https://github.com/rohandubal/FlappySwift.git
. - cd into the directory -
cd FlappySwift
- Open the Flappybird.xcodeproj using the command
open FlappyBird.xcodeproj
First, we will do a quick study of the game which we are going to modify. We are going to use FlappySwift, which is a swift implementation of the popular game Flappy Bird.
The game has been updated to have a navigation menu which lets the user see the High Scores
or select Play Game
.
Open the FlappyBird.xcodeproj
file in your Xcode IDE and hit CMD + R
which will run the app on your simulator or connected device.
The game should be in a working state for you to proceed to the next step.
We will be using AWS Amplify CLI
and AWS Console
to initialize resources in the cloud. If you have not already installed AWS Amplify CLI
, please do so by running the command npm install -g @aws-amplify/cli
and then configure the credentials for it by running amplify configure
.
- Run
amplify init
from the root directory ofFlappySwift
project (It is the folder which containsFlappyBird.xcodeproj
.) - Follow the prompts to give your project a name and select an IDE of your choice to edit your resources
- choose the type of app as
iOS
. - choose the profile which you created when you ran
aws configure
- You should an update in terminal which looks like
⠙ Initializing project in the cloud...
- Wait till you see the following message in your terminal before proceeding to next step:
✔ Successfully created initial AWS cloud resources for deployments.
Your project has been successfully initialized and connected to the cloud!
- Run
pod init
in the root directory of the App; this is to useCocoapods
as our dependency manager to install AWS iOS SDKs. If you do not have cocoapods installed, you can do that by runningsudo gem install cocoapods
. - Update the
Podfile
contents to following:# 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
- Run
pod install --repo-update
to install the dependencies; wait till the command finishes executing. - Close your current Xcode project window and open
FlappyBird.xcworkspace
from your project directory which was generated bypod install
command.
- The
User Authentication
feature is powered byAmazon Cognito
- We will add user authentication to our project by running the command
amplify auth add
- Select the
default configuration
when prompted - You should see a message like
Successfully added resource cognito98608300 locally
- Run
amplify push
and entery
when asked for confirmation - You should now see a message
⠧ Updating resources in the cloud. This may take a few minutes...
, please wait till the task completes. You can start the app integration by following next step. - When finished, you should see a message that says
✔ All resources are updated in the cloud
- From the Finder, drag
awsconfiguration.json
into Xcode IDE under the top Project Navigator folder (the folder name should match your Xcode project nameFlappyBird
). - When the Options dialog box that appears, do the following:
- Clear the Copy items if needed check box.
- Choose Create groups, and then choose finish.
- This configuration file contains information about your backend cloud resources.
-
In your Xcode project, open the
AppDelegate.swift
file. -
Add the import statement for
AWSMobileClient
import UIKit import AWSMobileClient @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { // ..... }
-
Initialize
AWSMobileClient
: Copy the following function and call it fromapplication(:didFinishLaunchingWithOptions)
Step 1: Copy the function in
AppDelegate
class ofAppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate { .... .... // Copy this function 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)") } } } }
Step 2: Call the copied function from
application(:didFinishLaunchingWithOptions)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Initialize AWSMobileClient self.initializeAWSMobileClient() return true }
-
Next, we add a sign-up and sign-in screen to the application.
Step 1: In
MainViewController.swift
, add the following extension toMainViewController
class:// Add AWSMobileClient import statement on the top import AWSMobileClient // Mark: AWS Sign In extension MainViewController { 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. } } }
Step 2: In the
viewDidLoad()
method ofMainViewController
callshowSignInScreenIfNotLoggedIn()
override func viewDidLoad() { super.viewDidLoad() // Call the method to show the sign in screen if the user is not signed in. self.showSignInScreenIfNotLoggedIn() }
Step 3: In the
signOutClicked()
method, callsignOut
and show the sign in screen again.@IBAction func onSignOutClicked(_ sender: Any) { // Call the sign out method and re-show the sign in screen AWSMobileClient.sharedInstance().signOut() self.showSignInScreenIfNotLoggedIn() }
-
That's it! Run your application to see it in action.
- The
User Analytics
feature is powered byAmazon Pinpoint
- We will add user analytics to our project by running the command
amplify analytics add
from terminal - Give a resource name of your choice (Enter key for default) and select
y
when promptedDo you want to allow guests and unauthenticated users to send analytics events?
- You should see a message like
Successfully added auth resource locally. Successfully added resource flappyswift locally
- Run
amplify push
and entery
when asked for confirmation - Wait till you see the following message in your terminal before proceeding to next step:
✔ All resources are updated in the cloud Pinpoint URL to track events https://console.aws.amazon.com/pinpoint/home/?region=us-east-1#/apps/8bd0b3fe4d5a441bb7629578f0fb834d/analytics/events
-
In your
AppDelegate
add a static variable forAWSPinpoint
client which we will use through out the app. Also add an import statement forAWSPinpoint
import UIKit import AWSMobileClient // Import Pinpoint import AWSPinpoint @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? // Create a Pinpoint variable static var pinpoint: AWSPinpoint? .....
-
Next, initialize the pinpoint instance in the completion handler of
AWSMobileClient.initialize()
func initializeAWSMobileClient() { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { print("UserState: \(userState.rawValue)") /****** Begin Code Copy ********/ // Initialize the pinpoint client. AppDelegate.pinpoint = AWSPinpoint(configuration: AWSPinpointConfiguration.defaultPinpointConfiguration(launchOptions: nil)) /****** End Code Copy ********/ } else if let error = error { print("error: \(error.localizedDescription)") } } }
-
Now that we have initialized our
Pinpoint
client, we will start recording events which help us understand app usage better. Copy the following extension inMainViewController.swift
:// 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!) } }
Here we have added functions to record how many times users are opening the high scores screen, how many times user is playing the game and if the user is a new login or a returning user.
-
Next, we call these functions from the appropriate places where we identify the occurance of these events.
Step 1: Call
logShowHighScore()
fromonHighScoresClicked()
method:@IBAction func onHighScoresClicked(_ sender: Any) { // Log an event when user clicks `High Scores` self.logShowHighScore() }
Step 2: Call
logPlayGame()
fromonPlayClicked()
method:@IBAction func onPlayClicked(_ sender: Any) { // Log an event when user clicks `Play` self.logPlayGame() }
Step 3: Call
logReturningOrNewUser()
fromviewDidLoad()
method:override func viewDidLoad() { super.viewDidLoad() self.showSignInScreenIfNotLoggedIn() // Log an event which determines if the user is a returning user or a new log in. self.logReturningOrNewUser() }
-
Now, run your application and open the high scores screen and play the game couple times so that we can get some analytics which we will view in the Pinpoint console later. Stop and restart the app once done.
- Run
amplify status
to make sure the appropriate resources are created in the cloud - Your resources table should look like
| Category | Resource name | Operation | Provider plugin |
| --------- | --------------- | --------- | ----------------- |
| Auth | cognito98608300 | No Change | awscloudformation |
| Analytics | flappyswift | No Change | awscloudformation |
- The
High Scores
feature is powered byAWS AppSync
(GraphQL API) andAmazon DynamoDB
- We will add user the high scores feature to our project by using the AWS Cloudformation Console.
- Go to CloudFormation Console and select a region of your choice if you do not want to continue with the default region.
- The cloud formation stack will create an
AWS AppSync
API which connects toAmazon DynamoDB
for a persistent store. It creates 2 operations in GraphQL API:CreateScore
andListHighScores
. Every user who plays the game will be creating their score entry in the backend using theCreateScore
API and retrieving the leader board usingListHighScores
. We will be doing a console walkthrough of theAWS AppSync
console to understand the GraphQL API and its functionality later in the tutorial.
- Select
Create Stack
from left side of page - Choose the option Upload a template to Amazon S3 and select the
ScoresGraphQLAPI.yaml
file from the root of the project and clickNext
- Give the stack a name of your choice and name the API
FlappyBirdScoreAPI
by specifying thegraphQlApiName
field - For
userPoolAwsRegion
anduserPoolId
, run the following command from terminalgrep -s "UserPoolId" amplify/backend/amplify-meta.json | awk '{print $2}'
- This will print your UserPoolId which looks like
us-east-2_DWSN28O1m
- Use the UserPoolId for
userPoolId
field and foruserPoolRegion
use the region which is prefixed to theUserPoolId
,us-east-2
in this case - In the next screen, click
Next
- Review the information on the screen and select
Create
. This will start creating the resources for the backend API - Once the resource creation completes, go to AppSync Console
- Click on your API which is named
FlappyBirdScoreAPI
- In the AppSync console, on home page of the
FlappyBirdScoreAPI
, copy the line which looks likeamplify add codegen --apiId r63frhxfe5gdladmioamvzxmd4
and run it in the terminal where we configured other cloud resources. - Follow the prompts to use defaults to enable the code generation. The output should look like:
✔ Getting API details Successfully added API FlappyBirdManual to your Amplify project ? Enter the file name pattern of graphql queries, mutations and subscriptions graphql/**/*.graphql ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes ? Enter the file name for the generated code API.swift ? Do you want to generate code for your newly created GraphQL API Yes ✔ Downloaded the schema ✔ Generated GraphQL operations successfully and saved at graphql ✔ Code generated successfully and saved in file API.swift
- This will create a file
API.swift
which you have to add in your Xcode project.
-
From the Finder, copy the
API.swift
file which contains theGraphQL
API models to your Xcode project. -
When the Options dialog box that appears, do the following:
- Clear the Copy items if needed check box.
- Choose Create groups, and then choose Next.
-
This
API.swift
file contains code generated swift models which are used for interacting withAWS AppSync
. -
In your
AppDelegate
add a static variable forAWSAppSync
client which we will use through out the app. Also add an import statement forAWSAppSync
import UIKit import AWSMobileClient import AWSPinpoint // Import AWSAppSync import AWSAppSync @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? static var pinpoint: AWSPinpoint? // Create a AWSAppSync variable static var appSyncClient: AWSAppSyncClient? .....
-
Next, initialize the
AWSAppSync
instance in the completion handler ofAWSMobileClient.initialize()
Step 1: Copy the
initializeAppSync()
function in yourAppDelegate
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)") } }
Step 2: Call the
initializeAppSync()
function from the completion handler ofAWSMobileClient.initialize()
func initializeAWSMobileClient() { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { print("UserState: \(userState.rawValue)") // Initialize the pinpoint client. AppDelegate.pinpoint = AWSPinpoint(configuration: AWSPinpointConfiguration.defaultPinpointConfiguration(launchOptions: nil)) /****** Begin Code Copy ********/ // Initialize AppSync client. self.initializeAppSync() /****** End Code Copy ********/ } else if let error = error { print("error: \(error.localizedDescription)") } } }
-
Update the
GameViewController.swift
file to create an entry for user score in Amazon DynamoDB via AWS AppSync for every game a user playsStep 1: Add the following new extension in
GameViewController.swift
file:// 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) } }
This extension captures the score created by the user.
Step 2: Update the
GameEnded
extension implementation to call the newly added methods:// 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 *********/ } }
-
Update the
ScoresViewController.swift
file to show the high scores to the user:Step 1: Add the
loadTopScores()
method inScoresViewController
which loads scores from AWS AppSync backendfunc 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 } } }) }
Step 2: Call the
loadTopScores()
method fromviewDidLoad()
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() }
This will make sure that
AppSync
is used to retrieve and display the leader board.
- Amazon Pinpoint for analytics events which are recorded
- Amazon Cognito Userpools console for registered users
- AWS AppSync console to demostrate resolvers and data sources