Now that the cloud-based backend is ready, let’s modify the application code to add an authentication screen. We’re going to make several changes in the application:
The view navigation will look like this:
We choose to write all AWS specific code in the AppDelegate
class, to avoid spreading dependencies all over the project. This is a design decision for this project, you may adopt other design for your projects. We use class extension mechanism to separate concerns (authentication, file access, API access) and make it possible to split concerns in multipe files. However, for this workshop, we kept all code in the AppDelegate.swift
class for easy copy / paste.
Let’s start to add a flag in the UserData
class to keep track of authentication status. We add line height to the existing code. You can copy/paste the whole content to replace Landmarks/Models/UserData.swift :
// Landmarks/Models/UserData.swift
import Combine
import SwiftUI
final class UserData: ObservableObject {
@Published var showFavoritesOnly = false
@Published var landmarks = landmarkData
@Published var isSignedIn : Bool = false
}
Add user authentication logic to Landmarks/AppDelegate.swift:
import SwiftUI
import ClientRuntime
import Amplify
import AWSCognitoAuthPlugin
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
// https://stackoverflow.com/questions/66156857/swiftui-2-accessing-appdelegate
static private(set) var instance: AppDelegate! = nil
public let userData = UserData()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppDelegate.instance = self
do {
// reduce verbosity of AWS SDK
SDKLoggingSystem.initialize(logLevel: .warning)
//Amplify.Logging.logLevel = .info
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure()
print("Amplify initialized")
// asynchronously
Task {
// check if user is already signed in from a previous run
let session = try await Amplify.Auth.fetchAuthSession()
// and update the GUI accordingly
await self.updateUI(forSignInStatus: session.isSignedIn)
}
// listen to auth events.
// see https://github.com/aws-amplify/amplify-ios/blob/dev-preview/Amplify/Categories/Auth/Models/AuthEventName.swift
let _ = Amplify.Hub.listen(to: .auth) { payload in
switch payload.eventName {
case HubPayload.EventName.Auth.signedIn:
Task {
print("==HUB== User signed In, update UI")
await self.updateUI(forSignInStatus: true)
}
// if you want to get user attributes
Task {
let authUserAttributes = try? await Amplify.Auth.fetchUserAttributes()
if let authUserAttributes {
print("User attribtues - \(authUserAttributes)")
} else {
print("Failed fetching user attributes failed")
}
}
case HubPayload.EventName.Auth.signedOut:
Task {
print("==HUB== User signed Out, update UI")
await self.updateUI(forSignInStatus: false)
}
case HubPayload.EventName.Auth.sessionExpired:
Task {
print("==HUB== Session expired, show sign in aui")
await self.updateUI(forSignInStatus: false)
}
default:
//print("==HUB== \(payload)")
break
}
}
} catch let error as AuthError {
print("Authentication error : \(error)")
} catch {
print("Error when configuring Amplify \(error)")
}
return true
}
}
// MARK: -- Authentication code
extension AppDelegate {
// change our internal state, this triggers an UI update on the main thread
@MainActor
func updateUI(forSignInStatus : Bool) async {
self.userData.isSignedIn = forSignInStatus
}
// signin with Cognito web user interface
public func authenticateWithHostedUI() async throws {
print("hostedUI()")
// UIApplication.shared.windows.first is deprecated on iOS 15
// solution from https://stackoverflow.com/questions/57134259/how-to-resolve-keywindow-was-deprecated-in-ios-13-0/57899013
let w = UIApplication
.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
let result = try await Amplify.Auth.signInWithWebUI(presentationAnchor: w!)
if (result.isSignedIn) {
print("Sign in succeeded")
} else {
print("Signin failed or required a next step")
}
}
// signout globally
public func signOut() async throws {
// https://docs.amplify.aws/lib/auth/signOut/q/platform/ios
let options = AuthSignOutRequest.Options(globalSignOut: true)
let _ = await Amplify.Auth.signOut(options: options)
print("Signed Out")
}
}
What did we add ?
line 2-4 : we import Amplify libraries. ClientRuntime is part of the AWS SDK, it is just required to change the logging verbosity of the AWS SDK for Swift.
line 24-25 : we initialize Amplify
line 37-75 : we add an Amplify.Hub.listen(to: .auth)
switch statement to listen for changes in authentication status. That code calls self.updateUI()
to update the isSignedIn
flag inside the userData
object. SwiftUI will automatically trigger a user interface refresh when the state of this object changes. You can learn more about SwiftUI binding in the SwiftUI documentation.
line 95-116 : we add an authenticateWithHostedUI()
method to trigger the UI flow using Cognito’s hosted web user interface.
line 119-125 : we add a signOut()
method to sign the user out.
Before proceeding to the next steps, build (⌘B) the project to ensure there is no compilation error.
In this section, we’re going to add a new application entry point: the LandingView. This view will check if the user is authenticated and will display either the authentication view or the main application view.
Let’s create two new Swift classes in $PROJECT_DIRECTORY/Landmarks
(same directory as AppDelegate.swift
or LandmarkList.swift
)
UserBadge
or LandmarkList
based on user’s authentication status.To add a new Swift class to your project, use Xcode menu and click File, then New or press ⌘N and then enter the file name.
Repeat the operation twice, once for UserBadge.swift
and once for LandingView.swift
The user badge is a very simple graphical view representing a big login button.
// UserBadge.swift
// Landmarks
import SwiftUI
struct UserBadge: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Circle().stroke(Color.blue, lineWidth: geometry.size.width/50.0)
VStack {
Circle()
.frame(width:geometry.size.width / 2.0, height:geometry.size.width / 2.0, alignment: .center)
.foregroundColor(.blue)
.offset(x:0, y:geometry.size.width/3.3)
Circle()
.frame(width:geometry.size.width, height:geometry.size.width, alignment: .center)
.foregroundColor(.blue)
.offset(x:0, y:geometry.size.width/3.0)
}
}
.clipShape(Circle())
.shadow(radius: geometry.size.width/30.0)
}
}
}
struct UserBadge_Previews: PreviewProvider {
static var previews: some View {
UserBadge()
}
}
This LandingView
selects the view to present based on authentication status. When user is not authenticated, it shows the UserBadge
. Clicking on the UserBadge
triggers the authenticateWithHostedUI()
method. When user is authenticated, it passes the user object to LandmarkList
.
Pay attention to the @ObservedObject
annotation. This tells SwiftUI to invalidate and redraw the View when the state of the object changes. When user signs in or signs out, LandingView
will automatically adjust and render the UserBadge
or the LandmarkList
view.
//
// LandingView.swift
// Landmarks
// Landmarks/LandingView.swift
import SwiftUI
struct LandingView: View {
@ObservedObject public var user : UserData
@EnvironmentObject private var appDelegate: AppDelegate
var body: some View {
return VStack {
// .wrappedValue is used to extract the Bool from Binding<Bool> type
if (!$user.isSignedIn.wrappedValue) {
Button(action: {
Task {
try await appDelegate.authenticateWithHostedUI()
}
}) {
UserBadge().scaleEffect(0.5)
}
} else {
LandmarkList().environmentObject(user)
}
}
}
}
struct LandingView_Previews: PreviewProvider {
static var previews: some View {
let userDataSignedIn = UserData()
userDataSignedIn.isSignedIn = true
let userDataSignedOff = UserData()
userDataSignedOff.isSignedIn = false
return Group {
LandingView(user: userDataSignedOff)
LandingView(user: userDataSignedIn)
}
}
}
Finally, we update LandmarkApp.swift
to launch our new LandingView
instead of launching LandmarkList
when the application starts. Highlighted lines show the update. You can copy/paste the whole content to replace Landmarks/LandmarkApp.swift :
//
// LandmarkApp.swift
// Landmarks
import SwiftUI
@main
struct LandmarkApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
LandingView(user: appDelegate.userData)
}
}
}
To make our tests easier and to allow users to signout and invalidate their session, let’s add a signout button on the top of the LandmarkList
view. Highlighted lines show the update. You can copy/paste the whole content to replace Landmarks/LandmarkList.swift.
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
A view showing a list of landmarks.
*/
import SwiftUI
struct SignOutButton : View {
@EnvironmentObject private var appDelegate: AppDelegate
var body: some View {
NavigationLink(destination: LandingView(user: appDelegate.userData)) {
Button(action: {
Task {
try await appDelegate.signOut()
}
}) {
Text("Sign Out")
}
}
}
}
struct LandmarkList: View {
@EnvironmentObject private var userData: UserData
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark)
.environmentObject(self.userData)
) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
.navigationBarItems(trailing: SignOutButton())
}
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone 13", "iPhone 14"], id: \.self) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
What we did just change ?
we created a SignOutButton
struct that has a reference to AppDelegate
and calls signOut()
when pressed. The button is just a text with a navigation link pointing to LandingView
we added that button as trailing item in the navigation bar.
Uppon sucessful authentication, the Cognito server redirects to the URI we provided when we configured Amplify authentication in step 3.1. We used the landmarks://
URI. We need to tell iOS to launch our app when a request is made for this URI.
To do this, we add landmarks://
to the app’s URL schemes:
In Xcode, right-click Info.plist and then choose Open As > Source Code.
Add the following entry (lines 6-16) in URL scheme:
<!-- YOUR OTHER PLIST ENTRIES HERE -->
<!-- ADD AN ENTRY TO CFBundleURLTypes for Cognito Auth -->
<!-- IF YOU DO NOT HAVE CFBundleURLTypes, YOU CAN COPY THE WHOLE BLOCK BELOW -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>landmarks</string>
</array>
</dict>
</array>
The complete file structure should look like this (redacted for brevity).
Before proceeding to the next steps, build (⌘B) the project to ensure there is no compilation error.