--- title: 'Build a User Management App with Swift and SwiftUI' description: 'Learn how to use Supabase in your SwiftUI App.' --- <$Partial path="quickstart_intro.mdx" /> ![Supabase User Management example](/docs/img/supabase-swift-demo.png) If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/swift-user-management). <$Partial path="project_setup.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} /> ## Building the app Build 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/supabase-swift) dependency. Add the `https://github.com/supabase/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 key that you copied [earlier](#get-api-details). 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. <$CodeTabs> ```swift name=Supabase.swift import Foundation import Supabase let supabase = SupabaseClient( supabaseURL: URL(string: "YOUR_SUPABASE_URL")!, supabaseKey: "YOUR_SUPABASE_PUBLISHABLE_KEY" ) ``` ### Set up a sign-in view Set up a SwiftUI view to manage sign-ins and sign-ups. Users should be able to sign in using a magic link. <$CodeTabs> ```swift name=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`. <$CodeTabs> ```swift name=ProfileView.swift import SwiftUI 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 .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 .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. <$CodeTabs> ```swift name=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 } } ``` ## Profile photos Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Add `PhotosPicker` 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: <$CodeTabs> ```swift name=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 <$CodeTabs> ```swift name=ProfileView.swift import PhotosUI import Storage 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) .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 .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 .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( filePath, data: data, options: FileOptions(contentType: "image/jpeg") ) return filePath } } ``` Finally, update your Models. <$CodeTabs> ```swift name=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. ### Launch! With all the views in place, add an entry point for the application. Add a new `AppView.swift` file. <$CodeTabs> ```swift name=AppView.swift import SwiftUI struct AppView: View { @State var isAuthenticated = false var body: some View { Group { if isAuthenticated { ProfileView() } else { AuthView() } } .task { for await state in supabase.auth.authStateChanges { 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. At this stage you have a fully functional application!