Using AWS as a Back End: Authentication & API

Learn how to use Amazon Web Services (AWS) to build a back end for your iOS apps with AWS Amplify and Cognito, using GraphQL. By Tom Elliott.

4.3 (3) · 1 Review

Download materials
Save for later
Share

Amazon Web Services (AWS) is a cloud computing platform. To support cloud computing, Amazon owns and operates data centers around the globe. It offers various infrastructure and software products “as a service”. For example, you can use Amazon EC2 to reserve virtual servers within Amazon’s data centers. Or you can use Amazon SageMaker to build and deploy machine learning models quickly and easily. AWS offers nearly 200 separate services, so whatever you need for your next project, you’re likely to find it!

As you work through the tutorial, you’ll learn how to use AWS Amplify to add authentication and database storage to a chat app called Isolation Nation.

This is an advanced-level tutorial. Before starting it, you should have a good understanding of Swift and SwiftUI. You should also have some understanding of both GraphQL and CocoaPods. If you need to brush up on any of these first, try the following tutorials:

Now it’s time to get cracking!

Getting Started

Isolation Nation is an app for people who are self-isolating due to COVID-19. It lets them request help from others in their local community. Isolation Nation works by asking the user for their postcode (the UK equivalent of a zip code) and adding it to a thread for their postcode area. For example, the full postcode for Buckingham Palace is SW1A 1AA. The postcode area is SW1A, and it represents the area shown here.

Users whose postcode is within the same area are added to a single thread. They can then send messages and replies to others in the same area.

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Open the workspace (not the project) for the IsolationNation starter project in Xcode.

Introduction to the App

Build and run the project. The app displays a list with a single-thread item, SW1A. Tap the item. The app will navigate to the message list for that location.

Threads screen

Tap any of the messages to see the list of replies for each message.

The app contains four main screens: Home, Threads, Messages and Replies. In Xcode, you can see a group for each screen in the Project navigator. You can also see a view model for each screen. These are outside their groups, so they’re easier to find.

The Project navigator showing files to be modified

Take a look around the project:

  • AppDelegate sets a logged-in user.
  • SceneDelegate sets RootView as the root view of the SwiftUI view hierarchy.
  • RootView checks for the existence of a signed-in user and, if one exists, loads HomeScreen.
  • HomeScreen loads ThreadsScreen.

The Threads, Messages and Replies screens all have a similar structure. Each one uses its view model as an ObservedObject to populate its view.

Open ThreadsScreenViewModel.swift. The view model contains a property, threadListState, which publishes an array of ThreadModel objects wrapped in a Loading enum. After the initializers, perform(action:) defines an API. This API allows a view to send a request to the view model to perform an action. The handlers for those actions follow.

On first inspection, the app may look like it’s already working. But notice how fetchThreads() simply returns a hard-coded list. The aim of this tutorial is to build a fully functional back end and remove all the hard-coded data. :]

Note: It will be easier to test the functionality in Isolation Nation if you use multiple simulators. I use Control Room to create a simulator with the same name as each of my users.

First, you need to sign up for an AWS account and install some software onto your computer.

Setting Up Your Environment

Open a browser and head over to the AWS Homepage.

Creating an AWS Account

If you already have an AWS account, sign in. Otherwise, click Create an AWS account in the top right corner, and sign up for the free tier. Once AWS creates your account, sign in as the Root user with the credentials you just created.

Signing in to AWS

Note: You need to provide a credit card as part of your sign-up. By following the tutorial, you should stay within the free limits. But it’s possible that AWS will charge a small fee for your product usage. After you complete the tutorial, you should remove all the resources you created to prevent any further charges.

Now it’s time to install the software prerequisites. Open Terminal.

First, make sure you have Git installed. It’s pre-installed on every modern macOS, so it should be. In your terminal, type the following:

git --version 

Make sure your version of Git is 2.14.1 or later. If not, you can install it here.

Next, check if you have Node v10 or later installed by running the following command in your terminal:

node --version

If you don’t have it installed, you can install it here.

And, finally, if you don’t have it already, install CocoaPods by running this command in your terminal:

sudo gem install cocoapods

Next, you’ll install and configure Amplify.

AWS Amplify

Amplify consists of three separate, but related, products:

  • First, there’s a Command Line Interface (CLI) for programmatically creating and reserving AWS resources on behalf of your project. AWS is powerful, but also complex. Amplify makes it easy!
  • Second, Amplify provides libraries for several popular programming environments, including iOS. These libraries provide simplified APIs for common app development use cases.
  • Finally, Amplify provides a limited set of UI components for quickly building out common user flows such as authentication. These components are currently not available for iOS.

Installing and Configuring Amplify

To get started with Amplify, install the CLI by typing the following into your terminal:

npm install -g @aws-amplify/cli amplify-app

The -g flag means the CLI will install globally on your computer, rather than just for a single project.

Note: This may take several minutes to complete and appear to be hung at times. Be patient and it will eventually complete.

Once you’ve installed the CLI, you must configure it so that it links to your AWS Account. Run the following in your terminal:

amplify configure

The terminal screen will ask you to log in to your AWS account, and a new browser tab will open. As requested by the CLI, press Enter to continue. Press Enter to select the default AWS region and type a username for your Amplify IAM user:

Configuring IAM in the AWS Console

Another browser tab will open automatically. Click Next: PermissionsNext: TagsNext: ReviewCreate user.

Note: On the success screen, copy your Access Key ID and Secret access key and save them somewhere safe. This is important, as you cannot retrieve them after closing this tab! These keys will allow the CLI to act as your recently created role.

Click Close.

Back in the terminal, follow the instructions to add your access key ID and secret access key.

Finally, create a profile name:

Configuring Amplify using your Terminal

Congratulations! Amplify is now set up on your computer. Next, you’ll add it to your app.

Adding Amplify to Your App

In Xcode, open Podfile. Between the # Pods for IsolationHelp comment and the end at the end of the file, add the following new dependencies:

pod 'Amplify'
pod 'Amplify/Tools'

Next, in your terminal, navigate to the root of the project directory. Type the following to install the dependencies in your project:

pod install --repo-update

Amplify is the main dependency. It provides your app with access to all the Amplify APIs. Amplify Tools adds various automation to Xcode’s build process to make working with Amplify easier.

Next, click the IsolationNation project in the project workspace, then the IsolationNation target.

In the Build Phases tab, click the plus button to add another phase. Choose New Run Script Phase.

Adding a Run Phase for Amplify Tools

Name the phase Amplify Tools by clicking the Run script title. Click and drag it above the Compile Sources phase.

Update the shell script to the following:

"${PODS_ROOT}/AmplifyTools/amplify-tools.sh"

Configuring the Amplify Tools Run Phase

To ensure all of the Amplify CLI tools operate correctly, enter the following command in Terminal:

npm i --package-lock-only
Note: If you’re using nvm to manage your node versions, the preceding command may be unnecessary.

Build your project. After the build completes, the Project navigator will have a new group called AmplifyConfig. This folder houses files containing configuration and resource identifiers for Amplify.

Amplify Config Group in Xcode

Next, in your terminal, type the following:

amplify init

Press Enter to accept the default project name, and select None as your default editor. When asked if you would like to use a profile, type Y and then choose the default profile.

Initializing Amplify

This will take some time to complete, as the CLI creates AWS resources for you.

Next, type the following into your terminal:

amplify console

This will open the Amplify Console in your browser. If you get an error that your project doesn’t exist, make sure you’ve selected the N. Virginia region.

The Amplify Console

At this point, you may want to look around the console to become familiar with it. For now, though, there’s not much to see, since you haven’t added any services to your app yet.

However, you’re about to turn Isolation Nation into a bona fide app! The first step is to add support for users to create accounts and log in. Amazon provides a service for this called Cognito. Cognito has a User Pool, which serves as a directory of all your users. You can configure your User Pool to allow users to log in with username and password, social identity providers like Google or Facebook, or enterprise security systems like SAML.

Configuring AWS Cognito

To start, open Podfile in Xcode and add the following dependency after the two existing dependencies:

pod 'AmplifyPlugins/AWSCognitoAuthPlugin'

Next, install the dependency by running this command in your terminal:

pod install --repo-update

Finally, use the Amplify CLI to configure Cognito for your project. Type the following command into the terminal window at the root of your project:

amplify add auth 

When the CLI prompts you, select Default configurationUsernameNo, I am done (the default option in each case), and wait for the Amplify CLI to complete.

Adding Cognito Auth to your Project

It’s important to note that the Amplify CLI has now configured Cognito for your project locally, but it has not saved that configuration in the cloud. You can confirm this by typing the following in the terminal:

amplify status

Checking Amplify Status

This tells you that you need to create a resource in the Auth category with the given name. Type the following into the terminal and confirm in the affirmative when asked:

amplify push

When you are asked if you would like to generate code for your new API, enter N.

This will likely take several minutes to complete as Amplify creates AWS resources for you.

Once it’s finished, head back to the Amplify Console in your browser. Select your app and then the Backend environments tab.

The Amplify console

An Authentication category now appears in your back end.

Click the Authentication link. Then, in the Users section, click the View in Cognito button to view the Cognito User Pool.

Note: If AWS presents a screen saying “Region not supported”, click Switch to US East (Virginia).

The Amplify Console - Authentication

Viewing User Pools in Cognito

Next, select App client settings in the left-hand menu. Copy the client ID and save it somewhere. You’ll need this later.

Selecting your ClientID

Phew! All your setup is now out of the way. Next, it’s time to add the code to your app to handle authentication.

Adding Authentication With Amazon Cognito

Open AppDelegate.swift. At the top of the file, after the UIKit import, add imports for Amplify:

import Amplify
import AmplifyPlugins

Remove the line that sets userSession.loggedInUser to “Lizzie”.

Immediately after initializing authenticationService, add the following:

do {
  try Amplify.add(plugin: AWSCognitoAuthPlugin())
  try Amplify.configure()

  #if DEBUG
  Amplify.Logging.logLevel = .debug
  #else
  Amplify.Logging.logLevel = .error
  #endif
} catch {
  print("Error initializing Amplify. \(error)")
}

This code configures the Amplify library with a Cognito authentication plug-in. Then it sets an appropriate log level for Amplify.

Build and run.

Oh no! Now the app displays a never-ending spinner! Clearly you’re not finished yet.

Selecting your ClientID

Completing the Authentication Service

Open AuthenticationService.swift. In the section identified by // MARK: Public API, you’ll see stub functions with names like signIn(as:identifiedBy:) and checkAuthSession(). Now it’s time to write some code that uses your Cognito back end.

First, add a new import at the top of the file:

import Amplify

Next, locate the empty checkAuthSession() and add the following implementation:

// 1
_ = Amplify.Auth.fetchAuthSession { [self] result in
  switch result {
  // 2
  case .failure(let error):
    logger.logError(error)
    signOut()

  // 3
  case .success(let session):
    if !session.isSignedIn {
      setUserSessionData(nil)
      return
    }

    // 4
    guard let authUser = Amplify.Auth.getCurrentUser() else {
      let authError = IsolationNationError.unexpctedAuthResponse
      logger.logError(authError)
      signOut()
      return
    }
    setUserSessionData(authUser.username)
  }
}

Here’s what this code does:

  1. Request the current auth session from Amplify.
  2. If there’s an error, sign the user out.
  3. On success, confirm the user is signed in.
  4. If the user is signed in, fetch the current user and set the details on the user session.

Build and run. The spinner is now replaced with a sign-in screen. :]

The Sign In Screen

Next, add the sign-in code. Remove all the code inside signIn(as:identifiedBy:), and replace it with the following:

return Future { promise in
  // 1
  _ = Amplify.Auth
    .signIn(username: username, password: password) { [self] result in
    switch result {
    // 2
    case .failure(let error):
      logger.logError(error.localizedDescription)
      promise(.failure(error))
    // 3
    case .success:
      guard let authUser = Amplify.Auth.getCurrentUser() else {
        let authError = IsolationNationError.unexpctedAuthResponse
        logger.logError(authError)
        signOut()
        promise(.failure(authError))
        return
      }
      // 4
      setUserSessionData(authUser.username)
    }
  }
}

This is what you’re doing:

  1. Call the Amplify sign-in API, passing the username and password.
  2. Check and handle failures.
  3. On success, fetch the current logged-in user.
  4. Set the user’s details on the user session, as before.

With this set up, users can sign in to your app!

But there’s just one problem: You don’t have any existing users, and there’s still no way to sign up. Time to fix that.

Replace the body of signUp(as:identifiedBy:with:) with the following:

return Future { promise in
  // 1
  let userAttributes = [AuthUserAttribute(.email, value: email)]
  let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
  // 2
  _ = Amplify.Auth.signUp(
    username: username, 
    password: password, 
    options: options
  ) { [self] result in
    DispatchQueue.main.async {
      switch result {
      case .failure(let error):
        logger.logError(error.localizedDescription)
        promise(.failure(error))
      case .success(let amplifyResult):
        // 3
        if case .confirmUser = amplifyResult.nextStep {
          promise(.success(.awaitingConfirmation(username, password)))
        } else {
          let error = IsolationNationError.unexpctedAuthResponse
          logger.logError(error.localizedDescription)
          promise(.failure(error))
        }
      }
    }
  }
}

In this code, you do the following:

  1. Configure a sign-up request to expect sign-up via email.
  2. Perform the sign-up using Amplify. You handle the result as you did in the previous examples.
  3. If sign-up is a success, return the awaitingConfirmation state. Amplify will send the user a code via email to confirm ownership of the address provided.

Next, you need to allow users to confirm their email address. Replace the contents of confirmSignUp(for:with:confirmedBy:) with this:

return Future { promise in
  // 1
  _ = Amplify.Auth.confirmSignUp(
    for: username, 
    confirmationCode: confirmationCode
  ) { [self] result in
    switch result {
    case .failure(let error):
      logger.logError(error.localizedDescription)
      promise(.failure(error))
    case .success:
      // 2
      _ = Amplify.Auth.signIn(
        username: username, 
        password: password
      ) { result in
        switch result {
        case .failure(let error):
          logger.logError(error.localizedDescription)
          promise(.failure(error))
        case .success:
          // 3
          checkAuthSession()
        }
      }
    }
  }
}

In this code, you verify the user:

  1. Confirm the sign-up with Amplify and handle the response in the usual fashion.
  2. On success, sign the user in.
  3. Call checkAuthSession(), which sets the user session.

Update signOut() to sign the user out of Cognito and clear their user session. Add the following code after setting the user session to nil:

_ = Amplify.Auth.signOut { [self] result in
  switch result {
  case .failure(let error):
    logger.logError(error)
  default:
    break
  }
}

Finally, open AppDelegate.swift. Add the following to the bottom of application(_:didFinishLaunchingWithOptions:), just before return true:

// Listen to auth changes
_ = Amplify.Hub.listen(to: .auth) { payload in
  switch payload.eventName {
  case HubPayload.EventName.Auth.sessionExpired:
    authenticationService.checkAuthSession()
  default:
    break
  }
}

By default, the authentication token returned from Cognito expires after an hour. But you can extend it without asking the user to log in again. That’s what’s happening here.

Build and run. This time, tap the Sign Up button and sign up with a username, email address and password.

Note: Most email providers will allow you to add an infinite number of suffixes per email address. For example, if your email address is example@gmail.com, example+lizzie@gmail.com will also route to your inbox. This is a great way to generate test email addresses.
Note: The default Cognito User Pool setup has a password policy of 8 characters or more.

The Sign Up Screen

AWS will send a confirmation code in an email to the address you provided. When that arrives, enter it and tap Confirm.

The Confirm Sign Up Screen

Congratulations! You have successfully logged into the app. :] Now, sign out and sign back in using the same user to verify that your code is working.

Next, confirm that your user now appears in the cloud. In your browser, go to your earlier Cognito tab. Click the Users and groups option in the left-hand menu. Select your user from the list (of one user!).

The cognito user dashboard

Find the sub field and save it somewhere. You’ll need this later.

GraphQL, AppSync and DynamoDB

Providing login capability in your app is a good start. But most products also need a way for users to modify and save data. For many modern apps, this data needs to be accessible to other users. A common solution to this problem is to persist the data in a database stored on a server somewhere in the cloud.

AWS provides many different database services, each with its own benefits and trade-offs. In this tutorial, you’ll use the document database DynamoDB. With DynamoDB, you don’t have to define a schema up front. This feature lends itself to quick prototyping or iterating on new ideas for your app.

Many mobile apps use a common architectural pattern in which the business and security logic resides in a back-end server. The app communicates with the server via a network API like GraphQL or REST. The server saves, retrieves or updates records in a database.

AppSync is an AWS service that generates both the database and the back end automatically, using your GraphQL schema. You define your model objects in the GraphQL schema, and AppSync generates the code for the back end. It then deploys the services needed to run your back end. And it creates the DynamoDB tables required to save your data.

Adding a GraphQL API

Back in Xcode, open Podfile and add the following dependency:

pod 'AmplifyPlugins/AWSAPIPlugin'

AWSAPIPlugin adds support to the Amplify Library for AppSync. Update your workspace by running the following from the command line:

pod install --repo-update

The first model object you’re going to add is a User object. Normally, Cognito handles user data. But Cognito’s data is private to the individual. And, in the Isolation Nation app, users must be able to see data about each other, such as their username. So this app needs a User Model as well as the Cognito data.

Defining Your Schema

In Xcode, open schema.graphql in the AmplifyConfig group. Replace the contents with the following:

# 1
type User
  # 4
  @model
{
  # 2
  id: ID!
  username: String!
  # 3
  sub: String!
  postcode: String
  createdAt: AWSDateTime!
}

Here’s what you’re doing:

  1. You declare a User type.
  2. You define various fields for the user type as code: Type tuples. For example, the id field is of type ID, and the postcode field is of type String. The ! operator signifies that a type is required.
  3. The sub field will contain the sub record from Cognito. This is the unique identifier for the user.
  4. You annotate the user type with the @model directive.

If you’re familiar with GraphQL, most of this will look pretty straightforward. However, there are a few bits unique to AppSync.

AppSync uses directives to provide a declarative API that allows you to specify how you want AppSync to configure each type or field. In this example, you specify that your user type is a model. This indicates to AppSync that it should create a DynamoDB table for this type.

Note: AppSync makes liberal use of GraphQL Directives. You’ll find a full list of directives here.

When you added Amplify Tools as a dependency in your project earlier, Amplify created an API on your behalf. However, this was before you added authentication to the project. Your API doesn’t know anything about the Cognito User Pool you just created. Before you continue, you need to fix that.

Generating the Database

In your terminal, run the following command:

amplify api update

When prompted, select GraphQLUpdate auth settingsAmazon Cognito User Pool, and then N.

Next, open amplifytools.xcconfig. Override the options by adding the code:

push=true
modelgen=true
profile=default
envName=dev

Here, you’re instructing the Amplify Tools script to take these actions:

  • Push changes to the cloud when it runs.
  • Generate models in Swift for your @model types.
  • Use your default AWS profile and the dev environment you created earlier.

Build your project in Xcode. If you open the Report navigator, you’ll see that the Amplify Tools script is now generating Swift models on your behalf.

Generating models

This build will take a long time! This is because the script is also running amplify push to generate all the necessary resources in the cloud.

Generating resources in AWS

When the build is complete, open the Project navigator. Confirm that a new group has been added to your project, and that it contains generated model files for your User model.

Swift generated models

In your browser, go back to the Amplify Console and reload the page. Select Backend environments and then click the API link.

Your api in the amplify console

On the API page, note the addition of a UserTable under the Data sources section. Click the View in AppSync button.

The User Table in the Amplify Console

On the AppSync page, open the Run a query accordion and click the Run a query button.

Running a Query from the AWS AppSync Console

This opens a GraphQL Playground that allows you to run live queries and mutations against your GraphQL API. Click the Login with User Pools button and sign in, using the Client ID you copied earlier and the credentials you created for your user. Press Login.

Login to your GraphQL API

Writing Data

Next, you’ll create a User model. First, if the Explorer pane is open, close it by clicking the X. Now, in the leftmost pane, add the following code, using the name of your user for username and the sub you copied earlier for both the id and sub fields:

mutation CreateUserMutation {
  createUser(input:{
    id: "389b0b66-f0e3-4907-9b4e-01ac4146bb0b"
    username: "Lizzie",
    sub: "389b0b66-f0e3-4907-9b4e-01ac4146bb0b",
  }) {
    id
    username
    sub
    createdAt
  }
}

Unfortunately, you can’t add comments to the GraphQL code. The following line numbers refer to those in the screenshot below.

  • On line 1, you define a named mutation called CreateUserMutation.
  • In lines 2–5, you run the createUser mutation generated by AppSync from your GraphQL schema. The mutation takes a single parameter called input. This parameter contains the username and sub for the user you want to create.
  • Lines 6–9 define the response to the mutation. You specify that id, username,
    sub and createdAt should be returned in the response. These are all fields of the created user.

Now press the orange Play button to run the mutation and create your user.

Creating a user in the GraphQL playground

Your mutation is sent to your AppSync GraphQL server, and the response appears in the middle column.

Next, select Data Sources in the left-hand menu. Then select the DynamoDB Resource for your UserTable.

Your browser will open a new tab. You’ll see your new DynamoDB database table. Select the Items tab.

The Items tab displays a single record. This is the user you just created. Now, click the record ID link to open the database record for your user.

Viewing a User record

Congratulations! You’ve just successfully created your first database record in DynamoDB. :]

Reading and Writing AppSync Data From Swift

Running GraphQL mutations in the Playground is fun — but not as fun as running them from your app!

Switch back to Xcode and open AppDelegate.swift. Before the call to Amplify.configure(), add the following line:

try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))

This tells the Amplify library to add support for AppSync via the API Plug-in and register the models created from your GraphQL schema. Currently, this is just your User model.

Reading Data From AppSync

At this point, the UserSession object is just a string representing the user’s name. In this next section, you’ll update the app to retrieve user data from your User model. You’ll use AppSync to read from your DynamoDB database. This will take quite a bit of refactoring, so don’t worry if you see Xcode errors as you work through this section.

Open UserSession.swift and update the two type declarations for loggedInUser. Change them from String? to User?:

public final class UserSession: ObservableObject {
  @Published var loaded = false
  // Here
  @Published var loggedInUser: User? {
    didSet {
      loaded = true
    }
  }

  init() {}

  // Here
  init(loggedInUser: User?) {
    self.loggedInUser = loggedInUser
  }
}

Next, open AuthenticationService.swift. Add the following method after setUserSessionData(_:):

private func fetchUserModel(id: String) -> Future<User, Error> {
  // 1
  return Future { promise in
    // 2
    _ = Amplify.API.query(request: .get(User.self, byId: id)) { [self] event in
      // 3
      switch event {
      case .failure(let error):
        logger.logError(error.localizedDescription)
        promise(.failure(error))
        return
      case .success(let result):
        // 4
        switch result {
        case .failure(let resultError):
          logger.logError(resultError.localizedDescription)
          promise(.failure(resultError))
          return
        case .success(let user):
          // 5
          guard let user = user else {
            let error = IsolationNationError.unexpectedGraphQLData
            logger.logError(error.localizedDescription)
            promise(.failure(error))
            return
          }
          promise(.success(user))
        }
      }
    }
  }
}

This might look a bit scary at first glance, but there’s really not much to it:

  1. First, this function returns a Future, which promises a User on successful completion.
  2. You use the Amplify API to run a query. The query will retrieve a User object by its ID.
  3. The API takes an event listener closure as its final argument. You issue the call with the result of the network request, which can either succeed or fail. On failure, you log the error before returning the failure.
  4. If the network request succeeds, you check the underlying GraphQL result type. This could still result in a failure, such as an invalid request, so again you must check for errors.
  5. If everything succeeds, you confirm that you received a valid user for your ID. If so, you return it.

Now, update setUserSessionData(_:) to take a User rather than a String:

private func setUserSessionData(_ user: User?) {
  DispatchQueue.main.async {
    if let user = user {
      self.userSession.loggedInUser = user
    } else {
      self.userSession.loggedInUser = nil
    }
  }
}

Then, in checkAuthSession(), replace the call to setUserSessionData(authUser.username) with the following:

let sub = authUser.userId
cancellable = fetchUserModel(id: sub)
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure(let error):
      logger.logError(error)
      signOut()
    case .finished: ()
    }
  }, receiveValue: { user in
    setUserSessionData(user)
  })

This code calls the fetchUserModel(id:) method you just wrote. On success, it sets the user session with the user.

Similarly, in signIn(as:identifiedBy:), replace the call to setUserSessionData(_:) with the following:

cancellable = self.fetchUserModel(id: authUser.userId)
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure(let error):
      signOut()
      promise(.failure(error))
    case .finished: ()
    }
  }, receiveValue: { user in
    setUserSessionData(user)
    promise(.success(.signedIn))
  })

Finally, open RootView.swift. Update the HomeScreenViewModel initializer in line 62 to use the new UserModel:

model: HomeScreenViewModel(
  userID: loggedInUser.sub, 
  username: loggedInUser.username)

Build and run. If you’re not already logged in, log in now. Confirm that the app still takes you to the Locations screen.

Nothing has changed in the UI. But your app is now using AppSync to query for the correct User model from the DynamoDB database!

Note: If you’re curious, you can prove that you’re now retrieving your user from the database by sniffing the HTTP traffic in your app. You can use a tool like Charles Proxy for this.

Viewing GraphQL Network Traffic

Creating Data in DynamoDB

Earlier, you created a new User record in DynamoDB by running a mutation in the GraphQL playground. You hard-coded the information for your one user. Obviously, that isn’t a good long-term solution! Instead, you should use Amplify.API. You’ll make that change now.

Open AuthenticationService.swift and locate the success handler in confirmSignUp(for:with:confirmedBy:). Remove the call to checkAuthSession(), and replace it with the following:

// 1         
guard let authUser = Amplify.Auth.getCurrentUser() else { 
  let authError = IsolationNationError.unexpctedAuthResponse         
  logger.logError(authError)        
  promise(.failure(IsolationNationError.unexpctedAuthResponse))      
  signOut()         
  return         
}        
// 2         
let sub = authUser.userId        
let user = User(         
  id: sub,        
  username: username,         
  sub: sub,       
  postcode: nil,      
  createdAt: Temporal.DateTime.now()      
)        
// 3         
_ = Amplify.API.mutate(request: .create(user)) { event in        
  switch event {         
  // 4       
  case .failure(let error):      
    signOut()       
    promise(.failure(error))         
  case .success(let result):         
    switch result {      
    case .failure(let error):      
      signOut()         
      promise(.failure(error))       
    case .success(let user):         
      // 5       
      setUserSessionData(user)      
      promise(.success(.signedIn))
    }                
  }      
}    

This is what your code does:

  1. First, you get the current user from the Amplify Auth API. If no user is logged in, you return an error and sign out.
  2. You create a new User model object, setting the username for your user. You set both id and sub to the userId from Cognito.
  3. Then you write this user model record to DynamoDB by calling the Amplify.API.mutate API with a create request type.
  4. You handle failures from the network layer and then the GraphQL layer, as in previous examples.
  5. Finally, you set the user session to your newly-created user and return a successful sign-in.

Build and run the app on a different simulator. Sign up as a new user. Confirm that the new user record appears in DynamoDB by refreshing the table in the DynamoDB tab in your browser.

Creating a second user

Viewing the User record for the second user

Where to Go From Here?

Congratulations! You’ve used AWS Cognito to add sign-up and sign-in to your app. And you’ve used AWS AppSync to read and write data between your app and a database stored in the cloud, via GraphQL.

You can download the finished project using the Download Material buttons. Remember that you’ll need to perform the Amplify setup for this project, just as you did for the starter project.

You can refer to the Amplify Framework Documentation to find out more about the AWS services available via Amplify. Or check out Part 2 of this tutorial, Using AWS as a Back End: The Data Store & Analytics. In it, you’ll learn how to use the DataStore API to build the rest of the Isolation Nation chat app with real-time updates and user analytics.

Note: Don’t forget to remove the AWS resources that you created to avoid getting charged