Quickstart: iOS (SwiftUI)
Build the notes app in SwiftUI: keep the session in the Keychain, follow sign-in state with auth.changes, and list and add the signed-in user's notes.
A SwiftUI app (iOS 15+) where people sign up, sign in, add notes and see only their own. The SDK uses Foundation only.
#Before you start
Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.
#1. Create the app and add the package
In Xcode create an App project (SwiftUI, Swift). Then File › Add Package Dependencies… and add the Sovaria package, product Sovaria.
https://github.com/sovaria/sovaria-swift.git (from: 1.0.0)#2. Keep the session in the Keychain
import Foundation
import Security
import Sovaria
final class KeychainStore: SessionStorage, @unchecked Sendable {
private func query(_ key: String) -> [String: Any] {
[kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "sovaria", kSecAttrAccount as String: key]
}
func read(_ key: String) async -> String? {
var q = query(key)
q[kSecReturnData as String] = true
var out: AnyObject?
guard SecItemCopyMatching(q as CFDictionary, &out) == errSecSuccess, let data = out as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
func write(_ key: String, _ value: String) async {
SecItemDelete(query(key) as CFDictionary)
var q = query(key)
q[kSecValueData as String] = Data(value.utf8)
q[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
SecItemAdd(q as CFDictionary, nil)
}
func delete(_ key: String) async { SecItemDelete(query(key) as CFDictionary) }
}#3. Connect and follow sign-in state
import Foundation
import Sovaria
@MainActor
final class NotesModel: ObservableObject {
@Published var signedIn = false
@Published var notes: [[String: Any]] = []
@Published var error = ""
private(set) var sovaria: Sovaria?
func start() async {
do {
// Restores the saved session (refreshing it if it's about to expire).
let client = try await Sovaria.initialize(url: "https://<project-ref>.sovarias.in",
anonKey: "<anon-key>", storage: KeychainStore())
sovaria = client
for await state in client.auth.changes { // first event: .initialSession
signedIn = state.session != nil
if signedIn { await load() }
}
} catch { self.error = "\(error)" }
}
func signIn(_ email: String, _ password: String, create: Bool) async {
guard let sovaria else { return }
do {
if create { _ = try await sovaria.auth.signUp(email: email, password: password) }
else { _ = try await sovaria.auth.signInWithPassword(email: email, password: password) }
} catch let e as SovariaError { error = e.message } catch { self.error = "\(error)" }
}
// Rows come back keyed by column name. The row rules add "where owner_id = auth.uid()".
func load() async {
do { notes = try await sovaria?.db.sql("select id, body from notes order by id desc").rows ?? [] }
catch { self.error = "\(error)" }
}
func add(_ body: String) async {
do {
_ = try await sovaria?.db.sql("insert into notes (body) values ($1)", [body])
await load()
} catch { self.error = "\(error)" }
}
func signOut() async { try? await sovaria?.auth.signOut() }
}#4. The screens
import SwiftUI
struct ContentView: View {
@StateObject private var model = NotesModel()
@State private var email = "", password = "", draft = ""
var body: some View {
Group {
if model.signedIn {
List {
HStack {
TextField("Write a note", text: $draft)
Button("Add") { Task { await model.add(draft); draft = "" } }
}
ForEach(model.notes.indices, id: \.self) { i in Text(model.notes[i]["body"] as? String ?? "") }
}
.toolbar { Button("Sign out") { Task { await model.signOut() } } }
} else {
Form {
TextField("Email", text: $email).textInputAutocapitalization(.never)
SecureField("Password", text: $password)
Button("Sign in") { Task { await model.signIn(email, password, create: false) } }
Button("Create account") { Task { await model.signIn(email, password, create: true) } }
}
}
if !model.error.isEmpty { Text(model.error).foregroundStyle(.red) }
}
.task { await model.start() }
}
}#5. Run it
Pick a simulator and press Run (⌘R). Create an account, add a note, sign out and back in: the note is still there, and a second account never sees it.
#Next steps
- Check who can read and change
notesbefore you ship: Tutorial: prove your table is private before you ship. - Push new notes to other devices as they happen: Database change events.
- Sign in with Apple, Google and magic links via deep links, plus push and App Attest: iOS quickstart (Swift).
- Every type and method: Swift SDK.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | Firebase iOS (Swift) setup |
| Supabase | Supabase Swift quickstart |
| Sovaria | Quickstart: iOS (SwiftUI) |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.