Skip to content
All essays
iOSMarch 27, 202513 min

Swift Persistence Frameworks: Choosing the Right Solution

Compare Swift persistence frameworks. SwiftData, Core Data, Realm, SQLite, and GRDB.

Ü
Ümit Uz
Mobile & Full Stack Developer

Choosing the right persistence framework is crucial for your app. Compare options and make informed decisions.

SwiftData

Modern Persistence

swift
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

swift
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

swift
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

swift
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

swift
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

swift
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

swift
// 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

  1. 1Choose Wisely: Select based on requirements
  2. 2Test Early: Prototype before committing
  3. 3Plan Migration: Design for future changes
  4. 4Monitor Performance: Profile database operations
  5. 5Handle Errors: Implement proper error handling
  6. 6Backup Data: Always have backup strategy
  7. 7Test Migrations: Verify data integrity
  8. 8Document Schema: Keep schema documentation

Choose the right framework for your needs!

Related essays

Next essay
SwiftUI State Management: Advanced Patterns