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
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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!

Viewing GraphQL Network Traffic

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.

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