Choosing the right persistence framework is crucial for your app. Compare options and make informed decisions.
SwiftData
Modern Persistence
import SwiftData
@Model
final class User {
var id: String
var name: String
var email: String
var createdAt: Date
init(id: String, name: String, email: String) {
self.id = id
self.name = name
self.email = email
self.createdAt = Date()
}
}
struct ContentView: View {
@Environment(.modelContext) private var modelContext
@Query private var users: [User]
var body: some View {
List {
ForEach(users) { user in
Text(user.name)
}
}
.task {
await loadUsers()
}
}
func loadUsers() async {
// SwiftData handles persistence automatically
}
func addUser(name: String, email: String) {
let user = User(id: UUID().uuidString, name: name, email: email)
modelContext.insert(user)
}
}Core Data
Traditional Persistence
import CoreData
class CoreDataManager {
static let shared = CoreDataManager()
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed: \(error)")
}
}
}
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func save() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
print("Error saving: \(error)")
}
}
}
}
// Usage
let user = User(context: context)
user.name = "John"
user.email = "john@example.com"
CoreDataManager.shared.save()Realm
Alternative Database
import RealmSwift
class User: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var name: String
@Persisted var email: String
@Persisted var createdAt: Date
convenience init(id: String, name: String, email: String) {
self.init()
self.id = id
self.name = name
self.email = email
self.createdAt = Date()
}
}
class RealmManager {
let realm: Realm
init() {
realm = try! Realm()
}
func addUser(_ user: User) {
try? realm.write {
realm.add(user)
}
}
func getUsers() -> Results<User> {
return realm.objects(User.self)
}
func updateUser(_ user: User) {
try? realm.write {
user.name = "Updated Name"
}
}
func deleteUser(_ user: User) {
try? realm.write {
realm.delete(user)
}
}
}SQLite
Direct SQL Access
import SQLite3
class SQLiteManager {
var db: OpaquePointer?
func openDatabase() -> Bool {
let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("database.sqlite")
if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
print("Error opening database")
return false
}
return true
}
func createTable() {
let createTableString = """
CREATE TABLE users(
id TEXT PRIMARY KEY,
name TEXT,
email TEXT
);
"""
var createTableStatement: OpaquePointer?
if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
if sqlite3_step(createTableStatement) == SQLITE_DONE {
print("Table created")
}
}
sqlite3_finalize(createTableStatement)
}
func insertUser(id: String, name: String, email: String) {
let insertStatementString = "INSERT INTO users (id, name, email) VALUES (?, ?, ?);"
var insertStatement: OpaquePointer?
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
sqlite3_bind_text(insertStatement, 1, (id as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 2, (name as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 3, (email as NSString).utf8String, -1, nil)
if sqlite3_step(insertStatement) == SQLITE_DONE {
print("User inserted")
}
}
sqlite3_finalize(insertStatement)
}
}GRDB
Type-Safe SQLite
import GRDB
struct User: Codable, FetchableRecord, PersistableRecord {
var id: String
var name: String
var email: String
var createdAt: Date
}
class GRDBManager {
var db: DatabaseQueue
init() throws {
let fileURL = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("db.sqlite")
db = try DatabaseQueue(path: fileURL.path)
try setupDatabase()
}
func setupDatabase() throws {
try db.write { db in
try db.create(table: "users") { t in
t.column("id", .text).primaryKey()
t.column("name", .text).notNull()
t.column("email", .text).notNull()
t.column("createdAt", .datetime).notNull()
}
}
}
func insertUser(_ user: User) throws {
try db.write { db in
try user.insert(db)
}
}
func fetchUsers() throws -> [User] {
try db.read { db in
try User.fetchAll(db)
}
}
}Comparison
Framework Comparison
| Feature | SwiftData | Core Data | Realm | SQLite | GRDB | |---------|-----------|-----------|-------|--------|------| | Ease of Use | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★★☆ | | Performance | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★★ | ★★★★★ | | Swift Integration | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★★★ | | Query Language | Swift | Predicates | Realm | SQL | SQL | | Migration | Auto | Manual | Auto | Manual | Manual | | Cross-Platform | No | No | Yes | Yes | Yes |
Choosing the Right Framework
Decision Guide
Choose SwiftData if:
- Building iOS 17+ apps
- Want modern Swift-first API
- Need SwiftUI integration
- Prefer automatic migrations
Choose Core Data if:
- Supporting older iOS versions
- Need mature, battle-tested solution
- Require complex relationships
- Want Apple's full ecosystem support
Choose Realm if:
- Need cross-platform support
- Want real-time sync
- Prefer object-oriented API
- Need reactive queries
Choose SQLite if:
- Need maximum performance
- Comfortable with SQL
- Want fine-grained control
- Building cross-platform apps
Choose GRDB if:
- Want SQLite with Swift safety
- Need type-safe queries
- Value Swift integration
- Prefer SQL over abstraction
Performance
Benchmarks
class PerformanceBenchmark {
func benchmarkInsert() {
let iterations = 10000
measure("SwiftData") {
for _ in 0..<iterations {
modelContext.insert(User(id: UUID().uuidString, name: "Test", email: "test@example.com"))
}
}
measure("Core Data") {
for _ in 0..<iterations {
let user = User(context: context)
user.id = UUID().uuidString
user.name = "Test"
user.email = "test@example.com"
}
try? context.save()
}
measure("Realm") {
try? realm.write {
for _ in 0..<iterations {
realm.add(User(id: UUID().uuidString, name: "Test", email: "test@example.com"))
}
}
}
}
func measure(_ name: String, block: () -> Void) {
let start = Date()
block()
let duration = Date().timeIntervalSince(start)
print("\(name): \(duration)s")
}
}Migration
Data Migration
// SwiftData Migration
enum VersionedSchema {
static var schema: Schema {
SchemaV1()
}
static var migrationPlan: any SchemaMigrationPlan.Type {
AppMigrationPlan.self
}
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
) { context in
// Custom migration logic
}
}Best Practices
- 1Choose Wisely: Select based on requirements
- 2Test Early: Prototype before committing
- 3Plan Migration: Design for future changes
- 4Monitor Performance: Profile database operations
- 5Handle Errors: Implement proper error handling
- 6Backup Data: Always have backup strategy
- 7Test Migrations: Verify data integrity
- 8Document Schema: Keep schema documentation
Choose the right framework for your needs!