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
- Go through the Amplify CLI Setup
If you don't want to use Pods in the steps below, checkout this guide:
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 apiNote:
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 modelsThe 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.