In the ever-evolving world of app development, SwiftUI has emerged as a powerful framework for building user interfaces, while Supabase offers a feature-rich backend service. In this article, we'll explore how to create a secure authentication system in SwiftUI using Supabase.
Before we dive into the implementation, make sure you have the following prerequisites in place:
- Xcode installed on your development machine.
- A Supabase account for authentication.
- Basic knowledge of SwiftUI and Swift programming.
If you haven't already, sign up for a Supabase account at https://supabase.io. Once you've registered, create a new project and note down your project URL and API key.
- Next, we'll configure Supabase to create a user authentication system. In your Supabase project dashboard:
- Go to Authentication > Settings and enable Email as a sign-in method.
- In Email drop down disable Confirm Email.
- Create a New SwiftUI Project
- Open Xcode and create a new SwiftUI project. Name it "SwiftUISupabaseAuth."
- Install Supabase Swift
- In your Xcode project, go to File > Swift Packages > Add Package Dependency. Enter the URL for the Supabase Swift SDK: /~https://github.com/supabase/supabase-swift.git
In the ever-evolving world of app development, SwiftUI has emerged as a powerful framework for building user interfaces, while Supabase offers a feature-rich backend service. In this article, we'll explore how to create a secure authentication system in SwiftUI using Supabase. Prerequisites Before we dive into the implementation, make sure you have the following prerequisites in place:
- Xcode installed on your development machine.
- A Supabase account for authentication.
- Basic knowledge of SwiftUI and Swift programming.
- Sign Up for Supabase If you haven't already, sign up for a Supabase account at https://supabase.io. Once you've registered, create a new project and note down your project URL and API key.
- Configure Supabase Next, we'll configure Supabase to create a user authentication system. In your Supabase project dashboard: Go to Authentication > Settings and enable Email as a sign-in method. In Email drop down disable Confirm Email.
Building the SwiftUI App Create a New SwiftUI Project Open Xcode and create a new SwiftUI project. Name it "SwiftUISupabaseAuth." Install Supabase Swift In your Xcode project, go to File > Swift Packages > Add Package Dependency. Enter the URL for the Supabase Swift SDK: /~https://github.com/supabase/supabase-swift.git
class SupabaseAuth {
let client = SupabaseClient(supabaseURL: URL(string: "Your url")!, supabaseKey: "your key")
func LoginUser() async throws {
do{
let session = try await client.auth.session
}catch let error{
throw error
}
}
func SignIn(email:String,password:String) async throws {
do{
try await client.auth.signIn(email: email.lowercased(), password: password)
}catch let error{
throw error
}
}
func SignUp(email:String,password:String) async throws{
do{
try await client.auth.signUp(email: email.lowercased(), password: password)
}catch let error{
throw error
}
}
func signOut() async throws{
do{
try await client.auth.signOut()
}catch let error{
throw error
}
}
}
The SupabaseAuth class is initialized with a SupabaseClient instance. The SupabaseClient is a Swift client library provided by Supabase, configured with your Supabase project's URL and API key. let client = SupabaseClient(supabaseURL: URL(string: "https://your-supabase-project-url.supabase.co")!, supabaseKey: "your-api-key") let supabaseAuth = SupabaseAuth(client: client)
The LoginUser method retrieves the current session of the authenticated user. It's a useful method to check if a user is already logged in without triggering a login process.
do {
let session = try await supabaseAuth.LoginUser()
// Handle user session
} catch let error {
// Handle error
}
The SignIn method allows a user to sign in with their email and password. Parameters: email: The user's email address. password: The user's password.
do {
try await supabaseAuth.SignIn(email: "user@example.com", password: "password123")
// User is signed in
} catch let error {
// Handle sign-in error
}
The SignUp method allows a user to create a new account with their email and password. Parameters: email: The user's email address. password: The user's password.
do {
try await supabaseAuth.SignUp(email: "newuser@example.com", password: "newpassword123")
// User account created
} catch let error {
// Handle sign-up error
}
The SignOut method allows the currently authenticated user to sign out.
do {
try await supabaseAuth.SignOut()
// User is signed out
} catch let error {
// Handle sign-out error
}
All methods in the SupabaseAuth class are marked as throws, which means they can throw errors. Proper error handling is essential to provide a smooth user experience in your application.
import Foundation
import Combine
import Supabase
// Define authentication states
enum AuthState: Hashable {
case Initial
case Signin
case Signout
}
class AuthViewModel: ObservableObject {
// Published properties for SwiftUI views
@Published var email: String = ""
@Published var password: String = ""
@Published var errorMessage: String = ""
@Published var authState: AuthState = AuthState.Initial
@Published var isLoading = false
// Cancellable set to manage Combine subscriptions
var cancellable = Set<AnyCancellable>()
// Initialize Supabase authentication
private var supabaseAuth: SupabaseAuth = SupabaseAuth()
// Check if the user is signed in
@MainActor
func isUserSignIn() async {
do {
try await supabaseAuth.LoginUser()
authState = AuthState.Signin
} catch _ {
authState = AuthState.Signout
}
}
// Sign up a new user
@MainActor
func signup(email: String, password: String) async {
do {
isLoading = true
try await supabaseAuth.SignUp(email: email, password: password)
authState = AuthState.Signin
isLoading = false
} catch let error {
errorMessage = error.localizedDescription
isLoading = false
}
}
// Sign in a user
@MainActor
func signIn(email: String, password: String) async {
do {
isLoading = true
try await supabaseAuth.SignIn(email: email, password: password)
authState = AuthState.Signin
isLoading = false
} catch let error {
errorMessage = error.localizedDescription
isLoading = false
}
}
// Validate email using a regular expression
func validEmail() -> Bool {
let emailRegex = #"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#
let isEmailValid = self.email.range(of: emailRegex, options: .regularExpression) != nil
return isEmailValid
}
// Validate password using a regular expression
func validPassword() -> Bool {
let passwordRegex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*\\.).{8,}$"
let isPasswordValid = self.password.range(of: passwordRegex, options: .regularExpression) != nil
return isPasswordValid
}
// Sign out a user
@MainActor
func signoutUser() async {
do {
try await supabaseAuth.signOut()
authState = AuthState.Signout
} catch let error {
errorMessage = error.localizedDescription
}
}
}
- AuthState Enum: This enum defines three authentication states - Initial, Signin, and Signout. It's used to track the user's authentication state.
- @Published Properties: These properties are marked with @Published, making them observable by SwiftUI views. Changes to these properties will automatically trigger view updates.
- Cancellable Set: It's used to manage Combine subscriptions, preventing memory leaks.
- SupabaseAuth: An instance of SupabaseAuth is used for authentication operations. It's assumed to be a custom class handling authentication.
- isUserSignIn(): Checks if the user is already signed in by attempting to log in silently. Updates authState accordingly.
- signup(): Signs up a new user with the provided email and password. It sets isLoading while the operation is in progress and handles errors.
- signIn(): Signs in an existing user with the provided email and password. Similar to signup(), it also handles errors and loading states.
- validEmail(): Validates the email using a regular expression to ensure it matches the expected format.
- validPassword(): Validates the password using a regular expression to enforce complexity requirements.
- signoutUser(): Signs out the currently authenticated user and updates the authState. Handles errors if any occur.
struct ContentView: View {
@EnvironmentObject private var authViewModel : AuthViewModel
@EnvironmentObject private var navigationViewModel : NavigationViewModel
var body: some View{
Group{
switch (authViewModel.authState) {
case .Initial:
Text("Loading")
case .Signin:
HomeView()
.environmentObject(authViewModel)
.environmentObject(navigationViewModel)
case .Signout:
LoginView()
.environmentObject(authViewModel)
.environmentObject(navigationViewModel)
}
}
.task {
await authViewModel.isUserSignIn()
}
}
}
-
@EnvironmentObject: This is a property wrapper provided by SwiftUI. It allows you to inject an object (in this case, AuthViewModel and NavigationViewModel) into the environment of a view. This means that the view will have access to these objects without needing to explicitly pass them.
-
Switch: This is a control flow statement. It checks the value of authViewModel.authState and performs different actions based on its value.
- Case .Initial: If the authentication state is Initial, it displays a Text view with the message "Loading". This likely indicates that the app is in the process of determining the user's authentication state.
- Case .Signin: If the authentication state is Signin, it displays the HomeView. Additionally, it injects both authViewModel and navigationViewModel as environment objects into HomeView.
- Case .Signout: If the authentication state is Signout, it displays the LoginView. Like with the HomeView, it injects authViewModel and navigationViewModel as environment objects into LoginView.
3. .task: This modifier is used to perform asynchronous work when the view appears. In this case, it calls the isUserSignIn method of authViewModel using the await keyword.
ContentView is a SwiftUI view responsible for managing the main content of your app based on the user's authentication state. Depending on whether the user is signed in or not, it displays either a loading message, the home view, or the login view. The environment objects authViewModel and navigationViewModel are provided to the child views (HomeView and LoginView) for them to use.