diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 6b625f1ad1..9a29fded31 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -318,6 +318,10 @@ export const gettingstarted: NavMenuConstant = {
name: 'Ionic Angular',
url: '/guides/getting-started/tutorials/with-ionic-angular',
},
+ {
+ name: 'Swift',
+ url: '/guides/getting-started/tutorials/with-swift',
+ },
],
},
],
diff --git a/apps/docs/pages/guides/auth/native-mobile-deep-linking.mdx b/apps/docs/pages/guides/auth/native-mobile-deep-linking.mdx
index c3ba884e73..a612d8b1a6 100644
--- a/apps/docs/pages/guides/auth/native-mobile-deep-linking.mdx
+++ b/apps/docs/pages/guides/auth/native-mobile-deep-linking.mdx
@@ -307,6 +307,44 @@ In certain auth scenarios you will need to handle linking back into your applica
+
+
+ ### Deep link config
+
+ 1. Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
+ 2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link.
+
+ The redirect callback url should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
+
+ 
+
+ Now add a cutom URL to your application, so the OS knows how to redirect back your application once the user clicks the magic link.
+
+ You have the option to use Xcode's Target Info Editor following [official Apple documentation](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme).
+
+ Or, declare the URL scheme manually in your `Info.plist` file.
+
+ ```xml Info.plist
+
+
+
+
+
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ io.supabase.user-management
+
+
+
+
+
+ ```
+
diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-swift.mdx
new file mode 100644
index 0000000000..15b988ae53
--- /dev/null
+++ b/apps/docs/pages/guides/getting-started/tutorials/with-swift.mdx
@@ -0,0 +1,527 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ title: 'Build a User Management App with Swift and SwiftUI',
+ description: 'Learn how to use Supabase in your SwiftUI App.',
+}
+
+
+
+
+
+
+
+If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/swift-user-management).
+
+
+
+
+
+## Building the App
+
+Let's start building the SwiftUI app from scratch.
+
+### Create a SwiftUI app in Xcode
+
+Open Xcode and create a new SwiftUI project.
+
+Add the [supabase-swift](https://github.com/supabase-community/supabase-swift) dependency.
+
+Add the `https://github.com/supabase-community/supabase-swift` package to your app. For instructions, see the [Apple tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
+
+Create a helper file to initialize the Supabase client.
+You need the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
+These variables will be exposed on the application, and that's completely fine since you have
+[Row Level Security](/docs/guides/auth#row-level-security) enabled on your database.
+
+```swift Supabase.swift
+import Supabase
+
+let supabase = SupabaseClient(
+ supabaseURL: URL(string: "YOUR_SUPABASE_URL")!,
+ supabaseKey: "YOUR_SUPABASE_ANON_KEY"
+)
+```
+
+### Set up a Login view
+
+Set up a SwiftUI view to manage logins and sign ups.
+Users should be able to sign in using a magic link.
+
+```swift AuthView.swift
+import SwiftUI
+import Supabase
+
+struct AuthView: View {
+ @State var email = ""
+ @State var isLoading = false
+ @State var result: Result?
+
+ var body: some View {
+ Form {
+ Section {
+ TextField("Email", text: $email)
+ .textContentType(.emailAddress)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ }
+
+ Section {
+ Button("Sign in") {
+ signInButtonTapped()
+ }
+
+ if isLoading {
+ ProgressView()
+ }
+ }
+
+ if let result {
+ Section {
+ switch result {
+ case .success:
+ Text("Check your inbox.")
+ case .failure(let error):
+ Text(error.localizedDescription).foregroundStyle(.red)
+ }
+ }
+ }
+ }
+ .onOpenURL(perform: { url in
+ Task {
+ do {
+ try await supabase.auth.session(from: url)
+ } catch {
+ self.result = .failure(error)
+ }
+ }
+ })
+ }
+
+ func signInButtonTapped() {
+ Task {
+ isLoading = true
+ defer { isLoading = false }
+
+ do {
+ try await supabase.auth.signInWithOTP(
+ email: email,
+ redirectTo: URL(string: "io.supabase.user-management://login-callback")
+ )
+ result = .success(())
+ } catch {
+ result = .failure(error)
+ }
+ }
+ }
+}
+```
+
+
+
+The example uses a custom `redirectTo` URL. For this to work, add a custom redirect URL to Supabase and a custom URL scheme to your SwiftUI application. Follow the guide on [implementing deep link handling](/docs/guides/auth/native-mobile-deep-linking?platform=swift).
+
+
+
+### Account view
+
+After a user is signed in, you can allow them to edit their profile details and manage their account.
+
+Create a new view for that called `ProfileView.swift`.
+
+```swift ProfileView.swift
+struct ProfileView: View {
+ @State var username = ""
+ @State var fullName = ""
+ @State var website = ""
+
+ @State var isLoading = false
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section {
+ TextField("Username", text: $username)
+ .textContentType(.username)
+ .textInputAutocapitalization(.never)
+ TextField("Full name", text: $fullName)
+ .textContentType(.name)
+ TextField("Website", text: $website)
+ .textContentType(.URL)
+ .textInputAutocapitalization(.never)
+ }
+
+ Section {
+ Button("Update profile") {
+ updateProfileButtonTapped()
+ }
+ .bold()
+
+ if isLoading {
+ ProgressView()
+ }
+ }
+ }
+ .navigationTitle("Profile")
+ .toolbar(content: {
+ ToolbarItem(placement: .topBarLeading){
+ Button("Sign out", role: .destructive) {
+ Task {
+ try? await supabase.auth.signOut()
+ }
+ }
+ }
+ })
+ }
+ .task {
+ await getInitialProfile()
+ }
+ }
+
+ func getInitialProfile() async {
+ do {
+ let currentUser = try await supabase.auth.session.user
+
+ let profile: Profile = try await supabase.database
+ .from("profiles")
+ .select()
+ .eq("id", value: currentUser.id)
+ .single()
+ .execute()
+ .value
+
+ self.username = profile.username ?? ""
+ self.fullName = profile.fullName ?? ""
+ self.website = profile.website ?? ""
+
+ } catch {
+ debugPrint(error)
+ }
+ }
+
+ func updateProfileButtonTapped() {
+ Task {
+ isLoading = true
+ defer { isLoading = false }
+ do {
+ let currentUser = try await supabase.auth.session.user
+
+ try await supabase.database
+ .from("profiles")
+ .update(
+ UpdateProfileParams(
+ username: username,
+ fullName: fullName,
+ website: website
+ )
+ )
+ .eq("id", value: currentUser.id)
+ .execute()
+ } catch {
+ debugPrint(error)
+ }
+ }
+ }
+}
+```
+
+### Models
+
+In `ProfileView.swift`, you used 2 model types for deserializing the response and serializing the request to Supabase. Add those in a new `Models.swift` file.
+
+```swift Models.swift
+struct Profile: Decodable {
+ let username: String?
+ let fullName: String?
+ let website: String?
+
+ enum CodingKeys: String, CodingKey {
+ case username
+ case fullName = "full_name"
+ case website
+ }
+}
+
+struct UpdateProfileParams: Encodable {
+ let username: String
+ let fullName: String
+ let website: String
+
+ enum CodingKeys: String, CodingKey {
+ case username
+ case fullName = "full_name"
+ case website
+ }
+}
+```
+
+### Launch!
+
+Now that you've created all the views, add an entry point for the application. This will verify if the user has a valid session and route them to the authenticated or non-authenticated state.
+
+Add a new `AppView.swift` file.
+
+```swift AppView.swift
+struct AppView: View {
+ @State var isAuthenticated = false
+
+ var body: some View {
+ Group {
+ if isAuthenticated {
+ ProfileView()
+ } else {
+ AuthView()
+ }
+ }
+ .task {
+ for await state in await supabase.auth.onAuthStateChange() {
+ if [.initialSession, .signedIn, .signedOut].contains(state.event) {
+ isAuthenticated = state.session != nil
+ }
+ }
+ }
+ }
+}
+```
+
+Update the entry point to the newly created `AppView`. Run in Xcode to launch your application in the simulator.
+
+## Bonus: Profile photos
+
+Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
+photos and videos.
+
+### Add PhotosPicker
+
+Let's add support for the user to pick an image from the library and upload it.
+Start by creating a new type to hold the picked avatar image:
+
+```swift AvatarImage.swift
+import SwiftUI
+
+struct AvatarImage: Transferable, Equatable {
+ let image: Image
+ let data: Data
+
+ static var transferRepresentation: some TransferRepresentation {
+ DataRepresentation(importedContentType: .image) { data in
+ guard let image = AvatarImage(data: data) else {
+ throw TransferError.importFailed
+ }
+
+ return image
+ }
+ }
+}
+
+extension AvatarImage {
+ init?(data: Data) {
+ guard let uiImage = UIImage(data: data) else {
+ return nil
+ }
+
+ let image = Image(uiImage: uiImage)
+ self.init(image: image, data: data)
+ }
+}
+
+enum TransferError: Error {
+ case importFailed
+}
+```
+
+#### Add PhotosPicker to Profile page
+
+```swift ProfileView.swift
+struct ProfileView: View {
+ @State var username = ""
+ @State var fullName = ""
+ @State var website = ""
+
+ @State var isLoading = false
+
++ @State var imageSelection: PhotosPickerItem?
++ @State var avatarImage: AvatarImage?
+
+ var body: some View {
+ NavigationStack {
+ Form {
++ Section {
++ HStack {
++ Group {
++ if let avatarImage {
++ avatarImage.image.resizable()
++ } else {
++ Color.clear
++ }
++ }
++ .scaledToFit()
++ .frame(width: 80, height: 80)
++
++ Spacer()
++
++ PhotosPicker(selection: $imageSelection, matching: .images) {
++ Image(systemName: "pencil.circle.fill")
++ .symbolRenderingMode(.multicolor)
++ .font(.system(size: 30))
++ .foregroundColor(.accentColor)
++ }
++ }
++ }
+
+ Section {
+ TextField("Username", text: $username)
+ .textContentType(.username)
+ .textInputAutocapitalization(.never)
+ TextField("Full name", text: $fullName)
+ .textContentType(.name)
+ TextField("Website", text: $website)
+ .textContentType(.URL)
+ .textInputAutocapitalization(.never)
+ }
+
+ Section {
+ Button("Update profile") {
+ updateProfileButtonTapped()
+ }
+ .bold()
+
+ if isLoading {
+ ProgressView()
+ }
+ }
+ }
+ .navigationTitle("Profile")
+ .toolbar(content: {
+ ToolbarItem {
+ Button("Sign out", role: .destructive) {
+ Task {
+ try? await supabase.auth.signOut()
+ }
+ }
+ }
+ })
++ .onChange(of: imageSelection) { _, newValue in
++ guard let newValue else { return }
++ loadTransferable(from: newValue)
++ }
+ }
+ .task {
+ await getInitialProfile()
+ }
+ }
+
+ func getInitialProfile() async {
+ do {
+ let currentUser = try await supabase.auth.session.user
+
+ let profile: Profile = try await supabase.database
+ .from("profiles")
+ .select()
+ .eq("id", value: currentUser.id)
+ .single()
+ .execute()
+ .value
+
+ username = profile.username ?? ""
+ fullName = profile.fullName ?? ""
+ website = profile.website ?? ""
+
++ if let avatarURL = profile.avatarURL, !avatarURL.isEmpty {
++ try await downloadImage(path: avatarURL)
++ }
+
+ } catch {
+ debugPrint(error)
+ }
+ }
+
+ func updateProfileButtonTapped() {
+ Task {
+ isLoading = true
+ defer { isLoading = false }
+ do {
++ let imageURL = try await uploadImage()
+
+ let currentUser = try await supabase.auth.session.user
+
+ let updatedProfile = Profile(
+ username: username,
+ fullName: fullName,
+ website: website,
++ avatarURL: imageURL
+ )
+
+ try await supabase.database
+ .from("profiles")
+ .update(updatedProfile)
+ .eq("id", value: currentUser.id)
+ .execute()
+ } catch {
+ debugPrint(error)
+ }
+ }
+ }
+
++ private func loadTransferable(from imageSelection: PhotosPickerItem) {
++ Task {
++ do {
++ avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self)
++ } catch {
++ debugPrint(error)
++ }
++ }
++ }
++
++ private func downloadImage(path: String) async throws {
++ let data = try await supabase.storage.from("avatars").download(path: path)
++ avatarImage = AvatarImage(data: data)
++ }
++
++ private func uploadImage() async throws -> String? {
++ guard let data = avatarImage?.data else { return nil }
++
++ let filePath = "\(UUID().uuidString).jpeg"
++
++ try await supabase.storage
++ .from("avatars")
++ .upload(
++ path: filePath,
++ file: data,
++ options: FileOptions(contentType: "image/jpeg")
++ )
++
++ return filePath
++ }
+}
+```
+
+Finally, update your Models.
+
+```swift Models.swift
+struct Profile: Codable {
+ let username: String?
+ let fullName: String?
+ let website: String?
+ let avatarURL: String?
+
+ enum CodingKeys: String, CodingKey {
+ case username
+ case fullName = "full_name"
+ case website
+ case avatarURL = "avatar_url"
+ }
+}
+```
+
+You no longer need the `UpdateProfileParams` struct, as you can now reuse the `Profile` struct for both request and response calls.
+
+### Storage management
+
+
+
+At this stage you have a fully functional application!
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/public/img/supabase-swift-demo.png b/apps/docs/public/img/supabase-swift-demo.png
new file mode 100644
index 0000000000..e66509183f
Binary files /dev/null and b/apps/docs/public/img/supabase-swift-demo.png differ
diff --git a/examples/user-management/swift-user-management/AppView.swift b/examples/user-management/swift-user-management/AppView.swift
new file mode 100644
index 0000000000..1cfc17573b
--- /dev/null
+++ b/examples/user-management/swift-user-management/AppView.swift
@@ -0,0 +1,35 @@
+//
+// AppView.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import SwiftUI
+
+struct AppView: View {
+ @State var isAuthenticated = false
+
+ var body: some View {
+ Group {
+ if isAuthenticated {
+ ProfileView()
+ } else {
+ AuthView()
+ }
+ }
+ .task {
+ for await state in await supabase.auth.onAuthStateChange() {
+ if [.initialSession, .signedIn, .signedOut].contains(state.event) {
+ isAuthenticated = state.session != nil
+ }
+ }
+ }
+ }
+}
+
+#if swift(>=5.9)
+ #Preview {
+ AppView()
+ }
+#endif
diff --git a/examples/user-management/swift-user-management/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/user-management/swift-user-management/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000000..eb87897008
--- /dev/null
+++ b/examples/user-management/swift-user-management/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/user-management/swift-user-management/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/user-management/swift-user-management/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..532cd729c6
--- /dev/null
+++ b/examples/user-management/swift-user-management/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,63 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/user-management/swift-user-management/Assets.xcassets/Contents.json b/examples/user-management/swift-user-management/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/examples/user-management/swift-user-management/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/user-management/swift-user-management/AuthView.swift b/examples/user-management/swift-user-management/AuthView.swift
new file mode 100644
index 0000000000..da30eb631a
--- /dev/null
+++ b/examples/user-management/swift-user-management/AuthView.swift
@@ -0,0 +1,80 @@
+//
+// AuthView.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import Supabase
+import SwiftUI
+
+struct AuthView: View {
+ @State var email = ""
+ @State var isLoading = false
+ @State var result: Result?
+
+ var body: some View {
+ Form {
+ Section {
+ TextField("Email", text: $email)
+ .textContentType(.emailAddress)
+ .autocorrectionDisabled()
+ #if os(iOS)
+ .textInputAutocapitalization(.never)
+ #endif
+ }
+
+ Section {
+ Button("Sign in") {
+ signInButtonTapped()
+ }
+
+ if isLoading {
+ ProgressView()
+ }
+ }
+
+ if let result {
+ Section {
+ switch result {
+ case .success: Text("Check you inbox.")
+ case let .failure(error): Text(error.localizedDescription).foregroundStyle(.red)
+ }
+ }
+ }
+ }
+ .onMac { $0.padding() }
+ .onOpenURL(perform: { url in
+ Task {
+ do {
+ try await supabase.auth.session(from: url)
+ } catch {
+ result = .failure(error)
+ }
+ }
+ })
+ }
+
+ func signInButtonTapped() {
+ Task {
+ isLoading = true
+ defer { isLoading = false }
+
+ do {
+ try await supabase.auth.signInWithOTP(
+ email: email,
+ redirectTo: URL(string: "io.supabase.user-management://login-callback")
+ )
+ result = .success(())
+ } catch {
+ result = .failure(error)
+ }
+ }
+ }
+}
+
+#if swift(>=5.9)
+ #Preview {
+ AuthView()
+ }
+#endif
diff --git a/examples/user-management/swift-user-management/AvatarImage.swift b/examples/user-management/swift-user-management/AvatarImage.swift
new file mode 100644
index 0000000000..191fb8aeca
--- /dev/null
+++ b/examples/user-management/swift-user-management/AvatarImage.swift
@@ -0,0 +1,55 @@
+//
+// AvatarImage.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import SwiftUI
+
+#if canImport(UIKit)
+ typealias PlatformImage = UIImage
+ extension Image {
+ init(platformImage: PlatformImage) {
+ self.init(uiImage: platformImage)
+ }
+ }
+
+#elseif canImport(AppKit)
+ typealias PlatformImage = NSImage
+ extension Image {
+ init(platformImage: PlatformImage) {
+ self.init(nsImage: platformImage)
+ }
+ }
+#endif
+
+struct AvatarImage: Transferable, Equatable {
+ let image: Image
+ let data: Data
+
+ static var transferRepresentation: some TransferRepresentation {
+ DataRepresentation(importedContentType: .image) { data in
+ guard let image = AvatarImage(data: data) else {
+ throw TransferError.importFailed
+ }
+
+ return image
+ }
+ }
+}
+
+extension AvatarImage {
+ init?(data: Data) {
+ guard let uiImage = PlatformImage(data: data) else {
+ return nil
+ }
+
+ let image = Image(platformImage: uiImage)
+ self.init(image: image, data: data)
+ }
+}
+
+enum TransferError: Error {
+ case importFailed
+}
diff --git a/examples/user-management/swift-user-management/Info.plist b/examples/user-management/swift-user-management/Info.plist
new file mode 100644
index 0000000000..50c754a761
--- /dev/null
+++ b/examples/user-management/swift-user-management/Info.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ io.supabase.user-management
+
+
+
+
+
diff --git a/examples/user-management/swift-user-management/Models.swift b/examples/user-management/swift-user-management/Models.swift
new file mode 100644
index 0000000000..f94f05ce80
--- /dev/null
+++ b/examples/user-management/swift-user-management/Models.swift
@@ -0,0 +1,22 @@
+//
+// Models.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import Foundation
+
+struct Profile: Codable {
+ let username: String?
+ let fullName: String?
+ let website: String?
+ let avatarURL: String?
+
+ enum CodingKeys: String, CodingKey {
+ case username
+ case fullName = "full_name"
+ case website
+ case avatarURL = "avatar_url"
+ }
+}
diff --git a/examples/user-management/swift-user-management/Preview Content/Preview Assets.xcassets/Contents.json b/examples/user-management/swift-user-management/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/examples/user-management/swift-user-management/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/user-management/swift-user-management/ProfileView.swift b/examples/user-management/swift-user-management/ProfileView.swift
new file mode 100644
index 0000000000..404958f339
--- /dev/null
+++ b/examples/user-management/swift-user-management/ProfileView.swift
@@ -0,0 +1,183 @@
+//
+// ProfileView.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import PhotosUI
+import Supabase
+import SwiftUI
+
+struct ProfileView: View {
+ @State var username = ""
+ @State var fullName = ""
+ @State var website = ""
+
+ @State var isLoading = false
+
+ @State var imageSelection: PhotosPickerItem?
+ @State var avatarImage: AvatarImage?
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section {
+ HStack {
+ Group {
+ if let avatarImage {
+ avatarImage.image.resizable()
+ } else {
+ Color.clear
+ }
+ }
+ .scaledToFit()
+ .frame(width: 80, height: 80)
+
+ Spacer()
+
+ PhotosPicker(selection: $imageSelection, matching: .images) {
+ Image(systemName: "pencil.circle.fill")
+ .symbolRenderingMode(.multicolor)
+ .font(.system(size: 30))
+ .foregroundColor(.accentColor)
+ }
+ }
+ }
+
+ Section {
+ TextField("Username", text: $username)
+ .textContentType(.username)
+ #if os(iOS)
+ .textInputAutocapitalization(.never)
+ #endif
+ TextField("Full name", text: $fullName)
+ .textContentType(.name)
+ TextField("Website", text: $website)
+ .textContentType(.URL)
+ #if os(iOS)
+ .textInputAutocapitalization(.never)
+ #endif
+ }
+
+ Section {
+ Button("Update profile") {
+ updateProfileButtonTapped()
+ }
+ .bold()
+
+ if isLoading {
+ ProgressView()
+ }
+ }
+ }
+ .onMac { $0.padding() }
+ .navigationTitle("Profile")
+ .toolbar(content: {
+ ToolbarItem {
+ Button("Sign out", role: .destructive) {
+ Task {
+ try? await supabase.auth.signOut()
+ }
+ }
+ }
+ })
+ .onChange(of: imageSelection) { _, newValue in
+ guard let newValue else { return }
+ loadTransferable(from: newValue)
+ }
+ }
+ .task {
+ await getInitialProfile()
+ }
+ }
+
+ func getInitialProfile() async {
+ do {
+ let currentUser = try await supabase.auth.session.user
+
+ let profile: Profile = try await supabase.database
+ .from("profiles")
+ .select()
+ .eq("id", value: currentUser.id)
+ .single()
+ .execute()
+ .value
+
+ username = profile.username ?? ""
+ fullName = profile.fullName ?? ""
+ website = profile.website ?? ""
+
+ if let avatarURL = profile.avatarURL, !avatarURL.isEmpty {
+ try await downloadImage(path: avatarURL)
+ }
+
+ } catch {
+ debugPrint(error)
+ }
+ }
+
+ func updateProfileButtonTapped() {
+ Task {
+ isLoading = true
+ defer { isLoading = false }
+ do {
+ let imageURL = try await uploadImage()
+
+ let currentUser = try await supabase.auth.session.user
+
+ let updatedProfile = Profile(
+ username: username,
+ fullName: fullName,
+ website: website,
+ avatarURL: imageURL
+ )
+
+ try await supabase.database
+ .from("profiles")
+ .update(updatedProfile)
+ .eq("id", value: currentUser.id)
+ .execute()
+ } catch {
+ debugPrint(error)
+ }
+ }
+ }
+
+ private func loadTransferable(from imageSelection: PhotosPickerItem) {
+ Task {
+ do {
+ avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self)
+ } catch {
+ debugPrint(error)
+ }
+ }
+ }
+
+ private func downloadImage(path: String) async throws {
+ let data = try await supabase.storage.from("avatars").download(path: path)
+ avatarImage = AvatarImage(data: data)
+ }
+
+ private func uploadImage() async throws -> String? {
+ guard let data = avatarImage?.data else { return nil }
+
+ let filePath = "\(UUID().uuidString).jpeg"
+
+ try await supabase.storage
+ .from("avatars")
+ .upload(
+ path: filePath,
+ file: data,
+ options: FileOptions(contentType: "image/jpeg")
+ )
+
+ return filePath
+ }
+}
+
+#if swift(>=5.9)
+ #Preview {
+ ProfileView()
+ }
+#endif
diff --git a/examples/user-management/swift-user-management/README.md b/examples/user-management/swift-user-management/README.md
new file mode 100644
index 0000000000..17e736b201
--- /dev/null
+++ b/examples/user-management/swift-user-management/README.md
@@ -0,0 +1,66 @@
+# Supabase Swift User Management
+
+This repo is a quick sample of how you can get started building apps using Swift and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Swift guide](https://supabase.io/docs/guides/with-swift).
+
+This repo will demonstrate how to:
+- Sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link)
+- Store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database)
+- Store image files in [Supabase storage](https://supabase.io/docs/guides/storage)
+
+
+
+## Getting Started
+
+Before running this app, you need to create a Supabase project and copy [your credentials](https://supabase.io/docs/guides/with-flutter#get-the-api-keys) to `Supabase.swift`.
+
+Run the application in a device or simulator using Xcode.
+
+## Database Schema
+
+```sql
+-- Create a table for public "profiles"
+create table profiles (
+ id uuid references auth.users not null,
+ updated_at timestamp with time zone,
+ username text unique,
+ avatar_url text,
+ website text,
+
+ primary key (id),
+ unique(username),
+ constraint username_length check (char_length(username) >= 3)
+);
+
+alter table profiles enable row level security;
+
+create policy "Public profiles are viewable by everyone."
+ on profiles for select
+ using ( true );
+
+create policy "Users can insert their own profile."
+ on profiles for insert
+ with check ( auth.uid() = id );
+
+create policy "Users can update own profile."
+ on profiles for update
+ using ( auth.uid() = id );
+
+-- Set up Realtime!
+begin;
+ drop publication if exists supabase_realtime;
+ create publication supabase_realtime;
+commit;
+alter publication supabase_realtime add table profiles;
+
+-- Set up Storage!
+insert into storage.buckets (id, name)
+values ('avatars', 'avatars');
+
+create policy "Avatar images are publicly accessible."
+ on storage.objects for select
+ using ( bucket_id = 'avatars' );
+
+create policy "Anyone can upload an avatar."
+ on storage.objects for insert
+ with check ( bucket_id = 'avatars' );
+```
\ No newline at end of file
diff --git a/examples/user-management/swift-user-management/Supabase.swift b/examples/user-management/swift-user-management/Supabase.swift
new file mode 100644
index 0000000000..c76a5509f8
--- /dev/null
+++ b/examples/user-management/swift-user-management/Supabase.swift
@@ -0,0 +1,14 @@
+//
+// Supabase.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import Foundation
+import Supabase
+
+let supabase = SupabaseClient(
+ supabaseURL: URL(string: "https://PROJECT_ID.supabase.co")!,
+ supabaseKey: "YOUR_SUPABASE_ANON_KEY"
+)
diff --git a/examples/user-management/swift-user-management/SwiftUIHelpers.swift b/examples/user-management/swift-user-management/SwiftUIHelpers.swift
new file mode 100644
index 0000000000..bda5b20384
--- /dev/null
+++ b/examples/user-management/swift-user-management/SwiftUIHelpers.swift
@@ -0,0 +1,18 @@
+//
+// SwiftUIHelpers.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import SwiftUI
+
+extension View {
+ func onMac(_ block: (Self) -> some View) -> some View {
+ #if os(macOS)
+ return block(self)
+ #else
+ return self
+ #endif
+ }
+}
diff --git a/examples/user-management/swift-user-management/UserManagement.entitlements b/examples/user-management/swift-user-management/UserManagement.entitlements
new file mode 100644
index 0000000000..625af03d99
--- /dev/null
+++ b/examples/user-management/swift-user-management/UserManagement.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.files.user-selected.read-only
+
+ com.apple.security.network.client
+
+
+
diff --git a/examples/user-management/swift-user-management/UserManagementApp.swift b/examples/user-management/swift-user-management/UserManagementApp.swift
new file mode 100644
index 0000000000..dbbe1f33ea
--- /dev/null
+++ b/examples/user-management/swift-user-management/UserManagementApp.swift
@@ -0,0 +1,17 @@
+//
+// UserManagementApp.swift
+// UserManagement
+//
+// Created by Guilherme Souza on 17/11/23.
+//
+
+import SwiftUI
+
+@main
+struct UserManagementApp: App {
+ var body: some Scene {
+ WindowGroup {
+ AppView()
+ }
+ }
+}
diff --git a/examples/user-management/swift-user-management/supabase-swift-demo.png b/examples/user-management/swift-user-management/supabase-swift-demo.png
new file mode 100644
index 0000000000..e66509183f
Binary files /dev/null and b/examples/user-management/swift-user-management/supabase-swift-demo.png differ