AWS Amplify Setup with SwiftUI

Jun 25, 2021 7 min read
AWS Amplify Setup with SwiftUI

Learn how to build cross-platform apps with real-time data sync backed by AWS security!

Goal

Build any app with it's API that shares real-time data across multi-platform devices.

This is how it works:

If you want to practice in an AWS Sandbox, here's the link.

Prerequisites

If you don't want to use Pods in the steps below, checkout this guide:
Swift Package Manager Support for Amplify | Amazon Web Services
This article was written by Kyle Lee, AWS Senior Developer Advocate Up until now, if you wanted to use AWS Amplify in your iOS app, you would have to install the Amplify Libraries using CocoaPods. Being the most popular dependency manager used for iOS projects written in Objective-C and/or Swift for…

If you decide to go with pods, go to your iOS project directory and declare the pods. Here are the Amplify pods:

Analytics
pod 'Amplify'
pod 'AmplifyPlugins/AWSPinpointAnalyticsPlugin'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin'
API (GraphQL)
pod 'Amplify'
pod 'AmplifyPlugins/AWSAPIPlugin'
API (REST)
pod 'Amplify'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin'
pod 'AmplifyPlugins/AWSAPIPlugin'
Authentication
pod 'Amplify'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin'
DataStore (w/o API)
pod 'Amplify'
pod 'AmplifyPlugins/AWSDataStorePlugin'
DataStore (w/ API)
pod 'Amplify'
pod 'AmplifyPlugins/AWSAPIPlugin'
pod 'AmplifyPlugins/AWSDataStorePlugin'
Note: If you want to use the Run Script method to handle model generation and configuration file setup, you should also include the following pod:
pod 'Amplify/Tools'
Predictions
pod 'Amplify'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin'
pod 'AWSPredictionsPlugin'
pod 'CoreMLPredictionsPlugin'
Storage
pod 'Amplify'
pod 'AmplifyPlugins/AWSS3StoragePlugin'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin'

This is how it would look for a basic auth app:

  ######## iOS/iPadOS ########
  target 'AppName (iOS)' do
  platform :ios, '14.0'

  use_frameworks!

  pod 'Amplify'
  pod 'AmplifyPlugins/AWSCognitoAuthPlugin'

  end

  # ######## MAC ########
  target 'AppName (macOS)' do
  platform :osx, '11.0'

  use_frameworks!

  end

Install the pods:

pod install --repo-update

Next, we need to initialize our Amplify project. Go to the same root dir. where the Pods are and do:

amplify init

After it's initialized, head over the AWS Amplify Console to see your newly created app and go to AWS Amplify section and on the home screen you'll see a section that says "Local setup instructions", unfold it and follow the steps to sync your local repo to the AWS Cloud.

You may see an error when you sync with the cloud such as: "folderNotFound: Amplify generated models not found at ..." which can be fixed as noted here . That is mainly for Data Store to work with data online-offline. Follow the instructions there.

Next is to add your components such as Auth

Going to the root of the project (where the pods are) and typing whatever component you'd like, in this case the Authentication:

Analytics
amplify add analytics
API (DataStore)
amplify add api
API (DataStore)
amplify add api
Note:
You would run this same command whether you are using GraphQL or a REST API. This step also encompasses DataStore since API is used to sync DataStore to the backend.

GraphQL:
If you're using GraphQL and have already configured your model schema, run the following:
amplify codegen models
The generated models can be found at amplify/generated/models and should be dragged and dropped into Xcode as part of your project.
Authentication
amplify add auth
Predictions
amplify add predictions
Storage
amplify add storage


After all the desired categories have been added to your app and properly configured, simply run the following:

amplify push

Once our config has been sent to the cloud, open up the .xcworkspace and make sure you have amplifyconfiguration.json and awsconfiguration.json in your Xcode project.

Next, go to the AWS Amplify Console and open up the Admin UI, and click on Authentication to update the settings you'd like, you can optionally reset it. After you hit Deploy, click on the top right icon to see the progress, it will give you when it's done a pull command so it updates in your project.

Configure Amplify in SwiftUI

Here's the official docs, however they use an odd implementation, below is a better one, also here's a video on how to do it.

EDIT: The best way is to use AppDelegate, below needs to be updated for it.

Main file
I'm setting up Amplify and declaring the session manager to work with:

import SwiftUI
import Amplify
import AmplifyPlugins

@main
struct Your_App_NameApp: App {
    
    @ObservedObject var authSessionManager = AuthSessionManager()
    
    init() {
        configureAmplify()
        authSessionManager.getCurrentAuthUser()
    }
    
    var body: some Scene {
        WindowGroup {
            MasterView()
                .environmentObject(authSessionManager)
        }
    }
    
    // Configure Amplify at start of the app
    private func configureAmplify() {
        do {
            // Amplify.Logging.logLevel = .verbose
            try Amplify.add(plugin: AWSCognitoAuthPlugin())
            try Amplify.configure()
            print("Amplify configured with auth plugin")

        } catch {
            print("Failed to initialize Amplify with \(error)")
        }
    }
}

AuthSessionManager

import Amplify
import Foundation

enum AuthState {
    case signUp
    case login
    case confirmCode(username: String)
    case session(user:AuthUser)
}

final class AuthSessionManager: ObservableObject {
    
    @Published var authState: AuthState = .login
    
    // Check if a current user is signed in
    func getCurrentAuthUser() {
        if let user = Amplify.Auth.getCurrentUser() {
            authState = .session(user: user)
        } else {
            authState = .login
        }
    }
    
    func showSignUp() {
        authState = .signUp
    }
    
    func showLogin() {
        authState = .login
    }
    
    func signUp(username: String, email: String, password: String) {
        let userAttributes = [AuthUserAttribute(.email, value: email)]
        let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
        
        _ = Amplify.Auth.signUp(
            username: username,
            password: password,
            options: options
        ) { [weak self] result in
            switch result {
            case .success(let signUpResult):
                print("Sign up result: ", signUpResult)
                
                switch signUpResult.nextStep {
                case .done:
                    print("Finished sign up")
                case .confirmUser(let details, _):
                    print(details ?? "no details")
                    
                    // Wrapped into an async queue so it when it finishes it brings back the state to the main thread ensuring there
                    // is no manipulation of the state
                    DispatchQueue.main.async {
                        // Confirm the Auth State to confirm code passing in the username to confirm
                        self?.authState = .confirmCode(username: username)
                    }
                }
            
            case .failure(let error):
                print("Sign up ERROR ", error)
            
            }
        }
        
    }
    
    func confirm(username: String, code: String) {
        _ = Amplify.Auth.confirmSignUp(
            for: username,
            confirmationCode: code
        ) { [weak self] result in
            switch result {
            case .success(let confirmResult):
                print(confirmResult)
                if confirmResult.isSignupComplete {
                    // Wrapped into an async queue so it when it finishes it brings back the state to the main thread ensuring there
                    // is no manipulation of the state
                    DispatchQueue.main.async {
                        self?.showLogin()
                    }
                }
            case .failure(let error):
                print("Failed to confirm code: ", error)
            }
        }
    }
    
    
    func login(username: String, password: String) {
        _ = Amplify.Auth.signIn(
            username: username,
            password: password
        ) { [weak self] result in
            switch result {
            case .success(let signInResult):
                print(signInResult)
                if signInResult.isSignedIn {
                    DispatchQueue.main.async {
                        self?.getCurrentAuthUser()
                    }
                }
            case .failure(let error):
                print("Login ERROR: ", error)
            }
        }
    }
   
    func signOut() {
        _ = Amplify.Auth.signOut { [weak self] result in
            switch result {
            case .success:
                DispatchQueue.main.async {
                    self?.getCurrentAuthUser()
                }
                
            case .failure(let error):
                print("Sign out ERROR ", error)
            }
        }
    }
}

MasterView

import SwiftUI

struct MasterView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    var body: some View {

        switch authSessionManager.authState {
        case .login:
            SignInView()
                .environmentObject(authSessionManager)
            
        case .signUp:
            SignUpView()
                .environmentObject(authSessionManager)
            
        // When user registers for the first time or tries to login
        case .confirmCode(let username):
            ConfirmationAuthView(username: username)
                .environmentObject(authSessionManager)
            
        // If user is signIn, show normal view
        case .session(let user):
            iOS_TabBarView(user: user)
                .environmentObject(authSessionManager)
        }
    }
}

struct MasterView_Previews: PreviewProvider {
    static var previews: some View {
        MasterView()
    }
}

SignInView

import SwiftUI

struct SignInView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        VStack {
            TextField("Username", text: $username)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            SecureField("Password", text: $password)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            Button(action: {
                authSessionManager.login(
                    username: username,
                    password: password
                )
            }){
                Text("Sign In")
            }
            
            Spacer()
            Button(action: {
                authSessionManager.showSignUp()
            }) {
                Text("Sign up")
            }
        }
    }
}

struct SignInView_Previews: PreviewProvider {
    static var previews: some View {
        SignInView()
    }
}

SignUpView

import SwiftUI

struct SignUpView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    @State var username: String = ""
    @State var email: String = ""
    @State var password: String = ""
    
    var body: some View {
        VStack {
            TextField("Username", text: $username)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            TextField("Email", text: $email)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            SecureField("Password", text: $password)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            Button(action: {
                authSessionManager.signUp(
                    username: username,
                    email:email,
                    password: password
                )
            }){
                Text("Sign Up")
            }
            
            Spacer()
            Button(action: {
                authSessionManager.showLogin()
            }) {
                Text("Log in")
            }
        }
    }
}

ConfirmationAuthView

import SwiftUI
import Amplify

struct ConfirmationAuthView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    @State var confirmationCode: String = ""
    
    let username: String
    
    var body: some View {
        VStack {
            Text("Username: \(username)")
            TextField("Confirmation Code: ", text: $confirmationCode)
                .padding()
                .cornerRadius(5.0)
                .padding(.bottom, 20)
            Button(action: {
                authSessionManager.confirm(
                    username: username,
                    code: confirmationCode
                )
            }){
                Text("Confirm")
            }
            
            Spacer()
            Button(action: {
                authSessionManager.showSignUp()
            }) {
                Text("Already have an account? Log in")
            }
        }
    }
}

iOS_TabBarView

import SwiftUI
import Amplify

struct iOS_TabBarView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    let user: AuthUser
    
    @State private var selectedTab: HostingBarCategories = .schedule
    
    var body: some View {
        
        TabView(selection: $selectedTab) {
            Text("Planner")
                .tag(HostingBarCategories.planner)
                .tabItem {
                    Image(systemName: "pencil.and.outline")
                    Text("Planner")
                }
            Text("To-dos")
                .tag(HostingBarCategories.todos)
                .tabItem {
                    Image(systemName: "checkmark")
                    Text("To-dos")
                }
            ScheduledView(user: user)
                .environmentObject(authSessionManager)
                .tag(HostingBarCategories.schedule)
                .tabItem {
                    Image(systemName: "calendar.circle.fill")
                    Text("Schedule")
                }
            Text("Goals")
                .tag(HostingBarCategories.goals)
                .tabItem {
                    Image(systemName: "flame")
                    Text("Goals")
                }
            Text("Menu")
                .tag(HostingBarCategories.menu)
                .tabItem {
                    Image(systemName: "slider.horizontal.3")
                    Text("Menu")
                }
        }
    }
}

HostingBarCategories

import Foundation

enum HostingBarCategories: Hashable, CaseIterable {

    case planner, todos, schedule, goals, menu

    var menuString: String { String(describing: self) }

    var tabs: Int {
        switch self {
        case .planner:    return 0
        case .todos:   return 1
        case .schedule:   return 2
        case .goals: return 3
        case .menu:   return 4
        }
    }

    var menuTitle: String {
        switch self {
        case .planner:    return "Planner"
        case .todos:   return "To-dos"
        case .schedule:   return "Schedule"
        case .goals: return "Goals"
        case .menu:   return "Menu"
        }
    }

    var menuIcon: String {
        switch self {
        case .planner:    return "pencil.and.outline"
        case .todos:   return "checkmark"
        case .schedule:   return "calendar.circle.fill"
        case .goals: return "flame"
        case .menu:   return "slider.horizontal.3"
        }
    }
}

ScheduledView

struct ScheduledView: View {
    
    @EnvironmentObject var authSessionManager: AuthSessionManager
    
    let user: AuthUser
    
    var body: some View {
        VStack {
            Spacer()
            Text("\(user.username) is signed in πŸ₯³")
                .font(.largeTitle)
                .padding()
            Spacer()
            Button("Sign Out", action: {
                authSessionManager.signOut()
            })
        }
    }
}

That's it! βœ… Full Auth implemented!

If you'd like to manage the data you can do it in User Management.

Great! Next, complete checkout for full access to ArturoFM.
Welcome back! You've successfully signed in.
You've successfully subscribed to ArturoFM.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.