Skip to content

Latest commit

 

History

History
539 lines (420 loc) · 23 KB

AWS_README.md

File metadata and controls

539 lines (420 loc) · 23 KB

Building Cloud Connected iOS Game

Pre-requisites

Setup the project

  • 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

Run the base game application

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.

Initialize cloud resources

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.

Initialize Project

  • Run amplify init from the root directory of FlappySwift project (It is the folder which contains FlappyBird.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!

Setup Application Dependencies

  • Run pod init in the root directory of the App; this is to use Cocoapods as our dependency manager to install AWS iOS SDKs. If you do not have cocoapods installed, you can do that by running sudo 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 by pod install command.

Setup Cloud Configuration

User Authentication

User Authentication Backend

  • The User Authentication feature is powered by Amazon 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 enter y 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

Setup configuration on App

  • From the Finder, drag awsconfiguration.json into Xcode IDE under the top Project Navigator folder (the folder name should match your Xcode project name FlappyBird).
  • 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.

User Authentication Client

  • 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 from application(:didFinishLaunchingWithOptions)

    Step 1: Copy the function in AppDelegate class of AppDelegate.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 to MainViewController 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 of MainViewController call showSignInScreenIfNotLoggedIn()

    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, call signOut 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.

User Analytics

User Analytics Backend

  • The User Analytics feature is powered by Amazon 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 prompted Do 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 enter y 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

User Analytics Client

  • In your AppDelegate add a static variable for AWSPinpoint client which we will use through out the app. Also add an import statement for AWSPinpoint

    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 in MainViewController.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() from onHighScoresClicked() method:

    @IBAction func onHighScoresClicked(_ sender: Any) {
        // Log an event when user clicks `High Scores`
        self.logShowHighScore()
    }

    Step 2: Call logPlayGame() from onPlayClicked() method:

    @IBAction func onPlayClicked(_ sender: Any) {
        // Log an event when user clicks `Play`
        self.logPlayGame()
    }

    Step 3: Call logReturningOrNewUser() from viewDidLoad() 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.

Verify Cloud Resources

  • 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 |

High Scores

  • The High Scores feature is powered by AWS AppSync(GraphQL API) and Amazon 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 to Amazon DynamoDB for a persistent store. It creates 2 operations in GraphQL API: CreateScore and ListHighScores. Every user who plays the game will be creating their score entry in the backend using the CreateScore API and retrieving the leader board using ListHighScores. We will be doing a console walkthrough of the AWS AppSync console to understand the GraphQL API and its functionality later in the tutorial.

High Scores Backend

  • 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 click Next
  • Give the stack a name of your choice and name the API FlappyBirdScoreAPI by specifying the graphQlApiName field
  • For userPoolAwsRegion and userPoolId, run the following command from terminal
    grep -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 for userPoolRegion use the region which is prefixed to the UserPoolId, 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

High Scores Client Code Generation

  • In the AppSync console, on home page of the FlappyBirdScoreAPI, copy the line which looks like amplify 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.

High Scores Client

  • From the Finder, copy the API.swift file which contains the GraphQL 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 with AWS AppSync.

  • In your AppDelegate add a static variable for AWSAppSync client which we will use through out the app. Also add an import statement for AWSAppSync

    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 of AWSMobileClient.initialize()

    Step 1: Copy the initializeAppSync() function in your AppDelegate

    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 of AWSMobileClient.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 plays

    Step 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 in ScoresViewController which loads scores from AWS AppSync backend

    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
                }
            }
        })
    }

    Step 2: Call the loadTopScores() method from 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()
    }

    This will make sure that AppSync is used to retrieve and display the leader board.

Console Walkthrough

  • Amazon Pinpoint for analytics events which are recorded
  • Amazon Cognito Userpools console for registered users
  • AWS AppSync console to demostrate resolvers and data sources