Xcode: The official IDE for iOS helps you develop and install the necessary tools to set it up.
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.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
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.
In this section, you’ll get FusionAuth up and running and use git
to create a new application.
First off, grab the code from the repository and change into that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-swift-ios-native.git
cd fusionauth-quickstart-swift-ios-native
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:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.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.
If you want to skip the step by step creation of the iOS App open the ./complete-application/
folder in Xcode.
Open Xcode and click File -> New Project . Choose iOS as the platform, and App as the application type. Click Next.
You can set Product Name to Quickstart
, Organization Identifier to io.fusionauth
and click Next and Create your project in a folder as per your preference.
Wait until Xcode has finished creating and indexing the project.
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.
We’ll use the FusionAuth iOS SDK, which simplifies integrating with FusionAuth and creating a secure web application.
Add the FusionAuth Swift SDK as a dependency to your project by Add Package Dependencies.. with the latest version. Select the Quickstart Application as a target and click Add Package
.
First, create a new property list file to store the configuration values. Select File -> New -> File from Template from the menu bar. Select Property List as the template. Click Next. Set the name of the file to FusionAuth.plist
. Make sure that the Quickstart
subfolder is selected. Click Create.
Right-click on the Quickstart/FusionAuth.plist
file in the project navigator and select Open As -> Source Code. Copy and paste the following code into the file to use the values provisioned by Kickstart.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>storage</key>
<string>keychain</string>
<key>additionalScopes</key>
<array>
<string>profile</string>
<string>email</string>
</array>
<key>fusionAuthUrl</key>
<string>http://localhost:9011</string>
<key>clientId</key>
<string>e9fdb985-9173-4e01-9d73-ac2d60d1dc8e</string>
</dict>
</plist>
In this example we are using a ObservableObject
to store and listen to changes in the FusionAuthState
object. This object will be used to determine which view to display based on the user’s authentication state.
Create a new Swift file to store the FusionAuthState
object. Select File -> New -> File from Template from the menu bar. Select Swift File as the template. Click Next. Set the name of the file to FusionAuthStateObject.swift
. Click Create.
Replace the contents of the Quickstart/FusionAuthStateObject.swift
file with the following code.
import Combine
import Foundation
import FusionAuth
/// FusionAuthStateObject is an observable object that manages the authorization state.
/// It listens for changes in the authorization state and updates its published property accordingly.
public class FusionAuthStateObject: ObservableObject {
/// The current authorization state.
@Published public var authState: FusionAuthState?
/// Initializes a new instance of FusionAuthStateObject.
public init() {
AuthorizationManager.instance.eventPublisher
.sink { [weak self] authState in
self?.authState = authState
}
.store(in: &cancellables)
}
/// Checks if the user is currently logged in.
/// - Returns: A boolean value indicating whether the user is logged in.
public func isLoggedIn() -> Bool {
guard let authState = self.authState else {
return false
}
return Date() < authState.accessTokenExpirationTime
}
/// A set of AnyCancellable to store the Combine subscriptions.
private var cancellables = Set<AnyCancellable>()
}
Finally, update the quickstart app Quickstart/QuickstartApp.swift
to use the AuthorizationManager
and register FusionAuthStateObject
as an environment object.
import SwiftUI
import FusionAuth
@main
struct QuickstartApp: App {
let fusionAuthState = FusionAuthStateObject()
init() {
AuthorizationManager.instance.initialize()
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(fusionAuthState)
.onOpenURL { url in
OAuthAuthorization.resume(with: url)
}
}
}
}
A View and controls are the visual building blocks of your app’s user interface. Use them to draw and organize your app’s content onscreen.
Start by creating login view by selecting File -> New -> File from Template from the menu bar. Select SwiftUI View as the template. Click Next. Set the name of the file to LoginView.swift
. Click Create. Replace the contents of the Quickstart/LoginView.swift
file with the following code.
import SwiftUI
import FusionAuth
struct LoginView: View {
@State private var errorWhileLogin = false
@State private var error: String?
var body: some View {
VStack {
Image("changebank")
.resizable()
.scaledToFit()
Text("Welcome to ChangeBank!")
Button("Login") {
Task {
do {
try await AuthorizationManager
.oauth()
.authorize(options: OAuthAuthorizeOptions())
} catch let error as NSError {
self.errorWhileLogin = true
self.error = error.localizedDescription
}
}
}.buttonStyle(PrimaryButtonStyle())
}
.padding()
.alert(
"Error occured while logging in",
isPresented: $errorWhileLogin,
presenting: error
) { _ in
Button("OK", role: .cancel) { errorWhileLogin = false }
} message: { error in
Text(error)
}
}
}
The LoginView
view is displayed when the user is not logged in and contains a button that initiates the login flow. The login
method is called when the login button is pressed.
Next, create the logged in view Quickstart/LoggedInView.swift
like previously done for the LoginView.swift
.
import SwiftUI
struct LoggedInView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Image("changebank")
.resizable()
.scaledToFit()
.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))
}
}
}
Next, we update the Quickstart/ContentView.swift
view to 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.
import SwiftUI
struct ContentView: View {
@EnvironmentObject private var authState: FusionAuthStateObject
var body: some View {
VStack {
if authState.isLoggedIn() {
LoggedInView()
} else {
LoginView()
}
}
.padding()
}
}
Create the main view after a successful login in the file Quickstart/HomeView.swift
.
import SwiftUI
import FusionAuth
struct HomeView: View {
@State var userInfo: UserInfo?
var body: some View {
if userInfo == nil {
VStack {
ProgressView()
.padding()
Text("Retrieving user info")
}
.onAppear {
getUserInfo()
}
} else {
VStack {
if userInfo?.given_name == nil || userInfo?.family_name == nil {
if userInfo?.email == nil {
Text("Welcome \(userInfo?.name ?? "") ").padding(.bottom, 20).font(.headline)
} else {
Text("Welcome \(userInfo?.email ?? "") ").padding(.bottom, 20).font(.headline)
}
} else {
Text("Welcome \(userInfo?.given_name ?? "") \(userInfo?.family_name ?? "")").padding(.bottom, 20).font(.headline)
}
Text("Your balance is:")
Text("$0.00").font(.largeTitle)
Button("Refresh token") {
Task {
do {
let accessToken = try await AuthorizationManager
.oauth()
.freshAccessToken()
guard let accessToken else {
print("Access token is not returned")
return
}
print("Refreshed access token: \(accessToken)")
} catch let error as NSError {
print(error)
}
}
}
Button("Log out") {
Task {
do {
try await AuthorizationManager
.oauth()
.logout(options: OAuthLogoutOptions())
} catch let error as NSError {
print(error)
}
}
}.buttonStyle(SecondaryButtonStyle())
}
}
}
func getUserInfo() {
Task {
do {
self.userInfo = try await AuthorizationManager
.oauth()
.userInfo()
} catch let error as NSError {
print("JSON decode failed: \(error.localizedDescription)")
}
}
}
}
The HomeView
view is displayed when the user is logged in. It greets the user by their name, email or nick retrieved from FusionAuth, and contains buttons that initiates the refresh token and logout flow. The UserInfo
state object is updated with the user’s information when the view is loaded, and the refreshToken
and logout
methods are called when the corresponding buttons are pressed.
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.
Create the Quickstart/MakeChangeView.swift
file.
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)"
}
}
In this section, you’ll update the look and feel of your native application to match the ChangeBank banking styling.
Add the colors and styles in the file Quickstart/Styles.swift
.
//
// Styles.swift
// Change Bank
//
import Foundation
import SwiftUI
struct PrimaryButtonStyle: ButtonStyle {
var 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(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 {
// swiftlint:disable:next identifier_name
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)
}
}
Now, add image assets to make this look like a real application with the following shell commands, run in the root of your project.
curl --create-dirs -o Quickstart/Assets.xcassets/AccentColor.colorset/Contents.json https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/AccentColor.colorset/Contents.json
curl --create-dirs -o Quickstart/Assets.xcassets/AppIcon.appiconset/Contents.json https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/AppIcon.appiconset/Contents.json
curl --create-dirs -o Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon.png https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon.png
curl --create-dirs -o Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon_dark.png https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon_dark.png
curl --create-dirs -o Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon_tinted.png https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/AppIcon.appiconset/fa_icon_tinted.png
curl --create-dirs -o Quickstart/Assets.xcassets/changebank.imageset/Contents.json https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/changebank.imageset/Contents.json
curl --create-dirs -o Quickstart/Assets.xcassets/changebank.imageset/Image.png https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-swift-ios-native/refs/heads/main/complete-application/Quickstart/Assets.xcassets/changebank.imageset/Image.png
Once you’ve created these files, you can test the application out.
The quickstart app is configured to run on an iOS Simulator or iOS device.
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 Refresh Token and you should see in the logs a new token being generated. Then click Log out. You should see another system notification and then be redirected to FusionAuth to log out. Finally, you’ll be returned to the login screen.
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 FusionAuth.plist
file to use the ngrok URL. Open the FusionAuth.plist
file in the project navigator, and change the value of the fusionAuthUrl
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.
Thank you for spending some time getting familiar with FusionAuth.
*Offer only valid in the United States and Canada, while supplies last.
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 gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
Make sure you have the right values at Quickstart/FusionAuth.plist
. Double-check the Issuer
in the Tenant to make sure it matches the URL that FusionAuth is running at including the protocol (https://).
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.