Supabase & SwiftUI: A Practical Example
Supabase SwiftUI Example
Alright, guys, let’s dive into building a cool app using Supabase and SwiftUI! If you’re like me, you’re always on the lookout for ways to make app development smoother and more efficient. Supabase, with its open-source Firebase alternative vibe, coupled with SwiftUI’s declarative approach, can be a real game-changer. In this article, we’ll walk through a practical example to get you started. Get ready to roll up your sleeves and get coding!
Table of Contents
Setting Up Your Supabase Project
First things first, let’s get our Supabase project up and running. Head over to the Supabase website and create a new project. Once you’ve named your project and selected a region, Supabase will start provisioning your database. This might take a few minutes, so grab a coffee and chill. While you’re waiting, think about what kind of data you want to store in your app. For our example, let’s imagine we’re building a simple to-do list app. We’ll need a table to store our to-do items, with columns for the task description, whether it’s completed, and maybe a user ID to keep track of who created the task.
Once your project is ready, navigate to the Table Editor in the Supabase dashboard. Create a new table named
todos
. Add the following columns:
-
id-UUID(primary key) -
task-TEXT -
completed-BOOLEAN -
user_id-UUID(foreign key referencing theauth.userstable)
Make sure to enable Row Level Security (RLS) on the
todos
table to ensure that users can only access their own to-do items. We’ll need to write a policy that allows users to
SELECT
,
INSERT
,
UPDATE
, and
DELETE
rows where the
user_id
matches their own user ID. This is crucial for keeping your data secure and preventing unauthorized access. To do this, go to the
Policies
tab for the
todos
table and create a new policy with the following settings:
-
Name:
Allow users to access their own todos -
Operation:
SELECT,INSERT,UPDATE,DELETE -
Using expression:
auth.uid() = user_id
This policy ensures that only the user who created the to-do item can access or modify it. With our Supabase project set up and our database configured, we’re ready to move on to the SwiftUI part of our app.
Creating a New SwiftUI Project
Now, let’s fire up Xcode and create a new SwiftUI project. Choose the “App” template and give your project a name, like “SupabaseTodo.” Make sure the interface is set to SwiftUI and the language is Swift. Once your project is created, you’ll see the default
ContentView.swift
file. This is where we’ll start building our UI. First, let’s add the Supabase Swift library to our project using Swift Package Manager. In Xcode, go to
File > Add Packages...
and enter the Supabase Swift GitHub repository URL:
https://github.com/supabase-community/supabase-swift
. Select the
supabase-swift
package and add it to your project.
With the Supabase library added, we can now initialize the Supabase client in our app. Open your
App.swift
file (the one with the
@main
annotation) and add the following code:
import SwiftUI
import Supabase
let supabaseURL = ProcessInfo.processInfo.environment["SUPABASE_URL"]!
let supabaseKey = ProcessInfo.processInfo.environment["SUPABASE_ANON_KEY"]!
let client = SupabaseClient(supabaseURL: supabaseURL, supabaseKey: supabaseKey)
@main
struct SupabaseTodoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(SessionManager())
}
}
}
Make sure to replace
YOUR_SUPABASE_URL
and
YOUR_SUPABASE_ANON_KEY
with your actual Supabase URL and anon key, which you can find in your Supabase project settings. We are also injecting a
SessionManager
into the environment. This
SessionManager
will handle the user’s session and authentication state. We will define the
SessionManager
later. Next, let’s create a model to represent our to-do items. Create a new file named
Todo.swift
and add the following code:
import Foundation
struct Todo: Identifiable, Codable {
let id: UUID
let task: String
let completed: Bool
let user_id: UUID
}
This struct defines the structure of our to-do items, with properties for the ID, task description, completion status, and user ID. Now that we have our Supabase client initialized and our to-do model defined, we can start building the UI for our app.
Building the UI with SwiftUI
Let’s head back to
ContentView.swift
and start building our UI. We’ll need a way to display the list of to-do items, add new items, and toggle the completion status of existing items. We will also need to authenticate the user before showing the
todos
. First, let’s create a
SessionManager
class that handles the authentication state of the user. Create a new file called
SessionManager.swift
:
import SwiftUI
import Supabase
class SessionManager: ObservableObject {
@Published var session: Session? = nil
init() {
loadSession()
}
func loadSession() {
Task {
do {
let session = try await client.auth.session
self.session = session
} catch {
print("Error loading session: (error)")
self.session = nil
}
}
}
func signIn(email: String, password: String) async throws {
let _ = try await client.auth.signIn(email: email, password: password)
loadSession()
}
func signUp(email: String, password: String) async throws {
let _ = try await client.auth.signUp(email: email, password: password)
loadSession()
}
func signOut() async throws {
try await client.auth.signOut()
session = nil
}
}
This class uses
@Published
to automatically update the UI when the
session
changes. The functions
signIn
,
signUp
, and
signOut
use the Supabase client to handle authentication. Now, let’s modify the
ContentView
to reflect these changes:
import SwiftUI
import Supabase
struct ContentView: View {
@EnvironmentObject var sessionManager: SessionManager
@State private var todos: [Todo] = []
@State private var newTask: String = ""
var body: some View {
if sessionManager.session != nil {
VStack {
List {
ForEach(todos) { todo in
HStack {
Text(todo.task)
Spacer()
Button(action: {
toggleCompletion(todo: todo)
}) {
Image(systemName: todo.completed ? "checkmark.square" : "square")
}
}
}
.onDelete(perform: deleteTodo)
}
HStack {
TextField("New task", text: $newTask)
Button(action: addTodo) {
Text("Add")
}
}
.padding()
Button(action: {
Task {
try await sessionManager.signOut()
}
}, label: {
Text("Sign Out")
})
}
.onAppear(perform: loadTodos)
} else {
AuthenticationView()
}
}
func loadTodos() {
Task {
do {
let response = try await client.from("todos").select().execute()
let todos = try JSONDecoder().decode([Todo].self, from: response.data)
self.todos = todos
} catch {
print("Error loading todos: (error)")
}
}
}
func addTodo() {
guard !newTask.isEmpty else { return }
Task {
do {
let userId = sessionManager.session!.user.id
let todo = Todo(
id: UUID(),
task: newTask,
completed: false,
user_id: userId
)
let _ = try await client.from("todos").insert(values: todo).execute()
self.newTask = ""
loadTodos()
} catch {
print("Error adding todo: (error)")
}
}
}
func toggleCompletion(todo: Todo) {
Task {
do {
var updatedTodo = todo
updatedTodo.completed.toggle()
let _ = try await client
.from("todos")
.update(values: updatedTodo)
.eq(column: "id", value: todo.id)
.execute()
loadTodos()
} catch {
print("Error toggling completion: (error)")
}
}
}
func deleteTodo(at offsets: IndexSet) {
Task {
do {
for index in offsets {
let todo = todos[index]
let _ = try await client
.from("todos")
.delete()
.eq(column: "id", value: todo.id)
.execute()
}
loadTodos()
} catch {
print("Error deleting todo: (error)")
}
}
}
}
Here, we’re using a
List
to display the to-do items, a
TextField
to add new items, and buttons to toggle the completion status and delete items. The
loadTodos
,
addTodo
,
toggleCompletion
, and
deleteTodo
functions interact with the Supabase database to fetch, create, update, and delete to-do items. The
AuthenticationView
is displayed if the user is not logged in. Let’s define it:
import SwiftUI
struct AuthenticationView: View {
@State private var email = ""
@State private var password = ""
@EnvironmentObject var sessionManager: SessionManager
var body: some View {
VStack {
TextField("Email", text: $email)
.padding()
.autocapitalization(.none)
SecureField("Password", text: $password)
.padding()
HStack {
Button("Sign In") {
Task {
do {
try await sessionManager.signIn(email: email, password: password)
} catch {
print("Sign in error: (error)")
}
}
}
.padding()
Button("Sign Up") {
Task {
do {
try await sessionManager.signUp(email: email, password: password)
} catch {
print("Sign up error: (error)")
}
}
}
.padding()
}
}
.padding()
}
}
This view allows users to sign in or sign up with their email and password. The
SessionManager
is used to handle the authentication logic. Remember to handle errors appropriately in a production app!
Running the App
Now, build and run your app. You should see a list of to-do items (initially empty) and a text field to add new items. Try adding a few to-do items, toggling their completion status, and deleting them. You should see the changes reflected in your Supabase database. Boom! You’ve successfully built a SwiftUI app that interacts with a Supabase database. You can also sign in and sign out.
Conclusion
So, there you have it! We’ve walked through a practical example of using Supabase and SwiftUI to build a simple to-do list app. We covered setting up your Supabase project, creating a new SwiftUI project, initializing the Supabase client, building the UI, and interacting with the database. This is just the beginning, guys. With Supabase and SwiftUI, the possibilities are endless. You can build all sorts of amazing apps, from social networks to e-commerce platforms to productivity tools. So go out there, experiment, and have fun building!
Remember to always handle errors gracefully, secure your data with RLS, and optimize your app for performance. And don’t be afraid to ask for help when you get stuck. The Supabase and SwiftUI communities are full of friendly and helpful people who are always willing to lend a hand. Happy coding!