native

iOS Swift

iOS Swift

In this quickstart, you are going to build an iOS app with Swift and SwiftUI and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native.

Prerequisites

  • Xcode: The official IDE for iOS development. Install it from the Mac App Store.
  • An Apple Developer account (optional): For running your app on a device and publishing your app. You can still develop and test the app in a simulator without an account.
  • Docker: The quickest way to stand up FusionAuth (there are other ways.) Ensure you also have Docker Compose installed.

This app was built using AppAuth, an open-source client SDK for communicating with OAuth 2.0 and OpenID Connect providers. AppAuth supports iOS 7 and newer.

General Architecture

While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

In this section, you’ll get FusionAuth up and running and use Git to create a new application.

Clone The Code

First, grab the code from the repository and change to that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native
cd fusionauth-quickstart-swift-ios-native

Run FusionAuth Via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example username is richard@example.com and the password is password.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Set Up A Public URL For FusionAuth

Your FusionAuth instance is now running on a different machine (your computer) than the mobile app will run (either a real device or an emulator), which means that it won’t be able to access localhost.

If the device and your computer are not connected to the same network or if you have something that blocks connections (like a firewall), learn how to expose a local FusionAuth instance to the internet. In summary, the process entails configuring ngrok on your local system, starting your FusionAuth instance on port 9011, and subsequently executing the following command.

ngrok http --request-header-add 'X-Forwarded-Port:443' 9011

This will generate a public URL that you can use to access FusionAuth when developing the app.

If the device (either real or emulator) and your computer are connected to the same network, you can use the local IP Address for your machine (for example, 192.168.15.2). Here are a few articles to help you find your IP address, depending on the operating system you are running:

Create Your iOS App

Now you will create an iOS app. While this section builds a simple iOS app using AppAuth, you can use the same configuration to integrate your existing app with FusionAuth.

Open Xcode and click File -> New Project . Choose “iOS” as the platform, and “App” as the application type. Click Next.

Set Product Name to ChangeBank. Set the Organization Identifier to FusionAuth.

If you use different values here, you will need to update the redirect URLs in your FusionAuth application to match your changes.

Click Next, and choose a location on your computer to save the project to.

Now you have a standard “Hello World” app. You can run it in the simulator by clicking the play button in the top left corner of Xcode or selecting Product -> Run from the menu bar.

Authentication

You’ll use the AppAuth Library, which simplifies integrating with FusionAuth and creating a secure web application.

Add The AppAuth Library

There are many ways to add the AppAuth library to your project. The simplest way without installing any additional tools is to add the package to the project.

Select File -> Add Package Dependency from the menu bar. In the search bar, paste in https://github.com/openid/AppAuth-ios. In the list of packages select “appauth-ios” and click Add Package. You will be asked to choose the packages to install. On the “Add to Target” column select Changebank for both “AppAuth” and “AppAuthCore”. Click Add Package to add the packages to your project.

Customization

Before you start coding, you need to add some assets and styles to your project that will be referenced later.

Add Assets And Styles

Add the ChangeBank logo to the project. Download it from here. Click on the Assets directory in the project navigator to open the asset manager. Drag and drop the downloaded changebank.png file into the asset manager.

Add an app icon. Download it from here. Click on the Assets directory in the project navigator to open the asset manager. Click the AppIcon item in the asset manager. Now drag and drop the downloaded AppIcon.png file into the AppIcon placeholder in the asset manager.

Add a new styles file to the project. Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to Styles.swift. Click Create.

Add the following code to the Styles.swift file:

//
//  Styles.swift
//  Change Bank
//

import Foundation
import SwiftUI

struct PrimaryButtonStyle: ButtonStyle {
    
    var color: Color = Color(red: 0.0353, green: 0.3882, blue: 0.1412)
    func makeBody(configuration: Configuration) -> some View {
        configuration.label.padding([
            .trailing, .leading], 48)
            .padding([.top, .bottom], 12).background(color)
            .foregroundColor(.white).clipShape(Capsule())
    }
}


struct SecondaryButtonStyle: ButtonStyle {
    var color: Color = .white
    var textColor: Color = Color(red: 0.0353, green: 0.3882, blue: 0.1412)
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding([.trailing, .leading], 48)
            .padding([.top, .bottom], 12)
            .background(color)
            .foregroundColor(textColor)
            .clipShape(Capsule())
            .overlay(Capsule().stroke(textColor, lineWidth: 2)) // overlay with Capsule stroke for the border
    }
}


struct CurrencyTextFieldStyle: TextFieldStyle {
    func _body(configuration: TextField<Self._Label>) -> some View {
        HStack {
            Text("$").foregroundColor(Color(red: 0.0353, green: 0.3882, blue: 0.1412))
            configuration
        }
        .padding(10)
        .overlay(
            Capsule().stroke(Color(red: 0.0353, green: 0.3882, blue: 0.1412), lineWidth: 2)
        )
        .keyboardType(.numberPad)
        .textContentType(.oneTimeCode)
    }
}

Add A P-List File

To store the settings necessary to connect to the FusionAuth instance, you need to add a Property List file (or “p-list file” for short) to the project. None of the settings stored are sensitive, so you can safely store them in an unencrypted p-list file.

Select File -> New -> File from the menu bar. Select Property List in the Resource section as the template. Click Next. Set the name of the file to ChangeBank. Click Create. The file will be opened in the p-list editor.

Expand the “Root” item by clicking the arrow > button, then click the + button next to the “Root” item. Name the new item authCredentials, and select the Type as Dictionary.

Now click the + button next to the “authCredentials” item. Name the new item clientId, and select the Type as String. In the “Value” column, enter e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.

Expand the “authCredentials” item by clicking the arrow > button. Click the + button next to the “authCredentials” item again. Name the new item issuer, and select the Type as String. In the “Value” column, enter http://localhost:9011.

To read the settings from the p-list file, you need to add a new class to the project. Select File -> New -> File from the menu bar. Select Swift File as the template and click Next. Set the name of the file to AppSettingsReader.swift. Click Create.

Add the following code to the AppSettingsReader.swift file:

//
//  AppSettingsReader.swift
//  ChangeBank
//

import Foundation


struct AppSettingsRoot : Decodable {
    internal let authCredentials : AuthCredentials
}

struct AuthCredentials : Decodable {
    internal let clientId : String
    internal let issuer : String
}


class AppSettingsReader {
    
    func loadAppSettings() -> AppSettingsRoot {
        let url = Bundle.main.url(forResource: "ChangeBank", withExtension:"plist")!
        let data = try! Data(contentsOf: url)
        let appSettings = try! PropertyListDecoder().decode(AppSettingsRoot.self, from: data)
        return appSettings
    }
}

This code adds new structs to hold the credentials and issuer settings, and a class to read the settings from the p-list file.

Implement AppAuth Logic

The AppAuth library was built for UIKit. To use it in a SwiftUI app, you need to implement some bridge logic between the two frameworks.

The main component to add is an AppDelegate class. This class will be responsible for coordinating the breakout browser window to handle the OAuth flow and interfacing with AppAuth.

Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to AppDelegate.swift. Click Create.

Add the following code to the AppDelegate.swift file:

//
//  AppDelegate.swift
//  Change Bank


import AppAuth
import SwiftUI

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var currentAuthorizationFlow: OIDExternalUserAgentSession?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {

        if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) {
            self.currentAuthorizationFlow = nil
            return true
        }

        return false
    }
    
}

The AppDelegate class extends UIResponder and adopts the UIApplicationDelegate protocol, which means it listens for events in the iOS app’s lifecycle.

In the application(_ app:UIApplication open url: URL options: ) method, the code checks if the URL being opened corresponds to an authorization flow and, if so, it tries to resume this flow. This flow relates to the situation where a user has completed a login or logout function in a web browser and the app is reopened via a callback URL from FusionAuth. The resumeExternalUserAgentFlow(with:) method is a part of AppAuth that handles the incoming URL and determines whether it corresponds to the AppAuth flow that was initiated, proceeding with the process if it matches.

The next step is to add an authentication flow manager class to the app. This class will be responsible for discovering the auth server endpoints and keys, initiating login and logout flows, and handling the results of those flows. It will also be responsible for storing the user’s authentication state and retrieving the user’s profile information from FusionAuth.

Select the File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to UserAuth.swift. Click Create.

Copy the following code into the UserAuth.swift file:

//
//  UserAuth.swift
//  ChangeBank
//

import Foundation
import SwiftUI
import AppAuth
import UIKit


class UserAuth: ObservableObject {
    
    @Published var isLoggedIn: Bool = false
    @Published var email: String = ""
    @Published var given_name: String = ""
    @Published var family_name: String = ""
    
    private let redirectUrl: String = "\(Bundle.main.bundleIdentifier ?? ""):/oauth2redirect/ios-provider";
    private var config : OIDServiceConfiguration?
    private var authState : OIDAuthState?
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    private let authStateKey : String = "authState"
    private let suiteName : String = "com.fusionauth.changebank"
    
    init() {
        loadState()
        discoverConfiguration()
    }
    
    
    // MARK: - Login and Logout
    
    func login() {
        
        let appSettings = AppSettingsReader().loadAppSettings()
        
        
        let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        let presentingView = windowScene?.windows.first?.rootViewController

        // Create redirectURI from redirectURL string
        guard let redirectURI = URL(string: redirectUrl) else {
            print("Error creating URL for : \(redirectUrl)")
            return
        }
        
        // Create login request
        let request = OIDAuthorizationRequest(configuration: config!, clientId: appSettings.authCredentials.clientId, clientSecret: nil, scopes: ["openid", "profile", "offline_access"],
                                        redirectURL: redirectURI, responseType: OIDResponseTypeCode, additionalParameters: nil)
        // performs authentication request
        appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: presentingView!) { (authState, error) in
            if let authState = authState {
                self.setAuthState(state: authState)
                self.saveState()
                self.fetchUserInfo()
            } else {
                print("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
            }
        }
    }
    
    func logout() {
        let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        let presentingView = windowScene?.windows.first?.rootViewController

        // Create redirectURI from redirectURL string
        guard let redirectURI = URL(string: redirectUrl) else {
            print("Error creating URL for : \(redirectUrl)")
            return
        }
        
        guard let idToken = authState?.lastTokenResponse?.idToken else { return }
        
        // Create logout request
        let request = OIDEndSessionRequest(configuration: config!, idTokenHint: idToken, postLogoutRedirectURL: redirectURI, additionalParameters: nil)

        guard let userAgent = OIDExternalUserAgentIOS(presenting: presentingView!) else { return }

        // performs logout request
        appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: userAgent, callback: { (_, error) in
            self.setAuthState(state: nil)
            self.saveState()
            self.email = "-"
            self.given_name = "-"
            self.family_name = "-"
        })
    }
    
    
    // MARK: - Loading & Saving State
    func loadState() {
        guard let data = UserDefaults(suiteName: suiteName)?.object(forKey: authStateKey) as? Data else {
            return
        }
        do {
            let authState = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? OIDAuthState
            self.setAuthState(state: authState)
            // Fetch user info if user authenticated
            fetchUserInfo()
       } catch {
           print(error)
       }
   }
    
    // Save user state to local
    func saveState() {
        guard let data = try? NSKeyedArchiver.archivedData(withRootObject: authState as Any, requiringSecureCoding: true) else {
            return
        }
        
        if let userDefaults = UserDefaults(suiteName: suiteName) {
            userDefaults.set(data, forKey: authStateKey)
            userDefaults.synchronize()
        }
    }
    
    // Set user auth state
    func setAuthState(state: OIDAuthState?) {
        if (authState == state) {
            return;
        }
        authState = state;
        isLoggedIn = state?.isAuthorized == true
    }
    
    
    // MARK: Get OIDC info
    
    func discoverConfiguration() {
        
        let appSettings = AppSettingsReader().loadAppSettings()
        guard let issuerUrl = URL(string: appSettings.authCredentials.issuer) else {
            print("Error creating URL for : \(appSettings.authCredentials.issuer)")
           return
        }
        // Get auth server endpoints
        OIDAuthorizationService.discoverConfiguration(forIssuer: issuerUrl) { configuration, error in
            if(error != nil) {
                print("Error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
            } else {
                self.config = configuration
            }
        }
    }
    
    func fetchUserInfo() {
        guard let userinfoEndpoint = authState?.lastAuthorizationResponse.request.configuration.discoveryDocument?.userinfoEndpoint else {
            print("Userinfo endpoint not declared in discovery document")
            return
        }

       print("Performing userinfo request")

        let currentAccessToken: String? = authState?.lastTokenResponse?.accessToken

        authState?.performAction() { (accessToken, idToken, error) in

            if error != nil  {
                print("Error fetching fresh tokens: \(error?.localizedDescription ?? "ERROR")")
                return
            }
            guard let accessToken = accessToken else {
                print("Error getting accessToken")
                return
            }
            if currentAccessToken != accessToken {
                print("Access token was refreshed automatically (\(currentAccessToken ?? "CURRENT_ACCESS_TOKEN") to \(accessToken))")
            } else {
                print("Access token was fresh and not updated \(accessToken)")
            }

            var urlRequest = URLRequest(url: userinfoEndpoint)
            urlRequest.allHTTPHeaderFields = ["Authorization":"Bearer \(accessToken)"]

            let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in

                DispatchQueue.main.async {
                    guard error == nil else {
                        print("HTTP request failed \(error?.localizedDescription ?? "ERROR")")
                        return
                    }
                    guard let response = response as? HTTPURLResponse else {
                        print("Non-HTTP response")
                        return
                    }
                    guard let data = data else {
                        print("HTTP response data is empty")
                        return
                    }

                    var json: [AnyHashable: Any]?

                    do {
                        json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
                    } catch {
                        print("JSON Serialization Error")
                    }

                    if response.statusCode != 200 {
                        let responseText: String? = String(data: data, encoding: String.Encoding.utf8)

                        if response.statusCode == 401 {
                            let oauthError = OIDErrorUtilities.resourceServerAuthorizationError(withCode: 0, errorResponse: json, underlyingError: error)
                            self.authState?.update(withAuthorizationError: oauthError)
                            print("Authorization Error (\(oauthError)). Response: \(responseText ?? "RESPONSE_TEXT")")
                        } else {
                            print("HTTP: \(response.statusCode), Response: \(responseText ?? "RESPONSE_TEXT")")
                        }
                        return
                    }
                    // Create profile info string
                    if let json = json {
                        self.email = json["email"] as! String
                        self.given_name = json["given_name"] as! String
                        self.family_name = json["family_name"] as! String
                    }
                }
            }
            task.resume()
        }
    }
}

Since the UserAuth class will be used by multiple views to determine the user’s logged in status and profile information, it is declared as an ObservableObject. This means that any SwiftUI views that use the UserAuth class will be notified when the UserAuth object changes. As we only want one instance of the UserAuth class, it should be instantiated in the main App class, and passed to the views that need it via the root view environmentObject method. This will make the UserAuth object available to all child views of the root view, and any views that need to access the UserAuth object can declare it as an @EnvironmentObject property.

Update the App class in the ChangBankApp.swift file to instantiate the UserAuth object and pass it to the root view:

//
//  Change_BankApp.swift
//  Change Bank
//

import SwiftUI

@main
struct ChangeBankApp: App {
    var userAuth = UserAuth()
    var body: some Scene {
    
        WindowGroup {
            ContentView()
                .environmentObject(userAuth)
                .preferredColorScheme(.light)
        }
    }
}

Create The ContentView View

The ContentView view is the root view of the app that will display the login screen if the user is not logged in, or the main app screens if the user is logged in. The UserAuth object is accessed via the @EnvironmentObject property wrapper, and the isLoggedIn property is used to determine which view to display.

Update the ContentView class in the ContentView.swift file as follows.

//
//  ContentView.swift
//  ChangeBank
//

import SwiftUI
import AppAuth


struct ContentView: View {
    @EnvironmentObject var userAuth: UserAuth

    var body: some View {
            if userAuth.isLoggedIn {
                
                // Top logo
                VStack(alignment: .leading) {
                    HStack {
                        Image("changebank")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 150)
                            .padding()
                        Spacer()
                    }
                    .frame(maxWidth: .infinity, alignment: .topLeading)
                    
                    
                    TabView {
                        HomeView().tabItem {
                            Label("Home", systemImage: "house")
                        }
                        MakeChangeView().tabItem {
                            Label("Make Change", systemImage: "centsign.circle")
                        }
                    }.accentColor(Color(red: 0.0353, green: 0.3882, blue: 0.1412))
                }
            } else {
                LoginView()
            }
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Create The LoginView View

The LoginView view is displayed when the user is not logged in and contains a button that initiates the login flow. The UserAuth object is accessed via the @EnvironmentObject property wrapper, and the login method is called when the login button is pressed.

Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to LoginView.swift. Click Create.

Copy the following code into the LoginView.swift file:

//
//  LoginView.swift
//  ChangeBank
//

import SwiftUI
import AppAuth



struct LoginView: View {
    @EnvironmentObject var userAuth: UserAuth
    var body: some View {
        VStack {
            Image("changebank")
                .resizable()
                .aspectRatio(contentMode: .fit)
            
            Text("Welcome to ChangeBank!")
            Button("Login"){
                userAuth.login()
            }.buttonStyle(PrimaryButtonStyle())
        }
        .padding()
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

Add The Logged In Home View

The HomeView view is displayed when the user is logged in. It greets the user by their name, retrieved from FusionAuth, and contains a button that initiates the logout flow. The UserAuth object is accessed via the @EnvironmentObject property wrapper, and its logout method is called when the logout button is pressed.

Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to HomeView.swift. Click Create.

Copy the following code into the HomeView.swift file:

//
//  HomeView.swift
//  ChangeBank
//

import SwiftUI

struct HomeView: View {
    @EnvironmentObject var userAuth: UserAuth
    var body: some View {
        VStack{
            Text("Welcome \(userAuth.given_name) \(userAuth.family_name)")
                .padding(.bottom, 20).font(.headline)
            Text("Your balance is:")
            Text("$0.00").font(.largeTitle)
            Button("Log out"){
                userAuth.logout()
            }.buttonStyle(SecondaryButtonStyle())
        
        }
    }
    
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}

Add The Make Change View

ChangeBank’s main business is to convert your notes to coins. The MakeChangeView view is available when the user is logged in. It takes an amount in dollars and returns the equivalent amount in nickels and pennies.

Select File -> New -> File from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to MakeChangeView.swift. Click Create.

Copy the following code into the MakeChangeView.swift file.

//
//  MakeChangeView.swift
//  ChangeBank
//

import SwiftUI

struct MakeChangeView: View {
    @State private var dollarValue: String = ""
    @State private var nickels: Int = 0
    @State private var pennies: Int = 0
    @State private var changeOutput: String = ""
    
    var body: some View {
        VStack {
            
            Text("Make Change").font(.largeTitle)
            
            TextField("Enter dollar value", text: $dollarValue)
                .textFieldStyle(CurrencyTextFieldStyle())
                .frame(maxWidth: 200)
            
            Button("Make Change") {
                makeChange()
            }
            .buttonStyle(PrimaryButtonStyle())
            
            Text(changeOutput).padding([.top, .leading, .trailing], 50).font(.callout)
            
        }
        .padding()
    }
    
    private func makeChange() {
        guard var value = Double(dollarValue) else {
            print("no dollars found :( \(dollarValue)")
            changeOutput = "Please enter a dollar amount to convert. "
            return
        }
        value = Double(Int(value * 100))/100 // truncate to 2 decimals to look like money
        let totalPennies = Int(value * 100)
        nickels = totalPennies / 5
        pennies = totalPennies % 5
        
        let pennyUnit = pennies != 1 ? "pennies" : "penny"
        let nickelUnit = nickels != 1 ? "nickels" : "nickel"
        
        changeOutput = "We can make change for $\(value) with \(nickels) \(nickelUnit) and \(pennies) \(pennyUnit)"
        
    }
}

struct MakeChangeView_Previews: PreviewProvider {
    static var previews: some View {
        MakeChangeView()
    }
}

Run The Application

You can run the application on a simulator or iOS device.

Running The App On A Simulator

To run the app in the simulator, click the play button in the top-left corner of Xcode or select Product -> Run from the menu bar. The app will be built and run in the simulator.

Click Login. You should see a system notification, and then be redirected to FusionAuth in a popup browser. Log in with the username richard@example.com and the password password. You should see the home screen displaying your name, your balance, and a logout button.

At the bottom of the screen, you’ll see a tab bar with two tabs. Click the Make Change tab. Enter an amount in dollars, and click Make Change. You should see the equivalent amount in nickels and pennies displayed.

Navigate back to the home screen by clicking the Home tab. Click Logout. You should see another system notification and then be redirected to FusionAuth to log out. Finally, you’ll be returned to the login screen.

Running The App On A Device

To run the app on your device, you need to make a few changes. This is because the app will be running on a different device to the FusionAuth instance, so it won’t be able to access the FusionAuth instance via localhost.

A simple way to get around this is to use ngrok, a tool that allows you to expose a local server to the internet. You can follow the guide here. Note the URL ngrok gave you as you’ll need it soon.

Now that you have the URL, you need to update the FusionAuth Tenant issuer to make sure it matches the given address.

Log in to the FusionAuth admin UI, browse to Tenants in the sidebar, and click on the “Default” tenant to edit it. Paste the complete address (with protocol and domain) you copied from ngrok into the Issuer field (for example, https://6d1e-2804-431-c7c9-739-4703-98a7-4b81-5ba6.ngrok-free.app). Save the tenant by clicking the icon in the top-right corner.

Navigate to Applications and click on the “Example Apple SwiftUI iOS App” application. Click on the JWT tab, change both Access token signing key and Id token signing key to Auto generate a new key on save... and save the application.

You must create new keys after modifying the Tenant because the Issuer field is embedded in the key.

Now you need to update the ChangeBank.plist file to use the ngrok URL. Open the ChangeBank.plist file in the project navigator, and change the value of the issuer key to the ngrok URL.

Now you can run the app on your device. Connect your device to your computer via USB. In Xcode, select your device from the device menu in the top-left corner of the window. Click the play button in the top left corner of Xcode, or select Product -> Run from the menu bar. The app will be built and run on your device.

Note that you need to enable developer mode on your device to run the app there. You will also need to associate a developer team with your Xcode project. You can do this by selecting the project file in the project navigator and selecting your team in the Signing & Capabilities tab. If you don’t have a developer account, you can use your personal Apple ID as the team.

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:

Security

Tenant and Application Management

Troubleshooting

  • I can’t log in.

Make sure you have the right values in the ChangeBank.plist file. Double-check the Issuer in the Tenant to make sure it matches the public URL that FusionAuth is running at.

  • It still doesn’t work.

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native.git
cd fusionauth-quickstart-swift-ios-native
docker compose up

Then open and run the Xcode project in the complete-application directory.