All Articles
CategoryiOS
Reading Time
14 min read
Published
2026-05-13
Word Count
1,765words

Grab a coffee — this one is a deep dive!

SwiftData in Production: 6 Months of Real-World Experience and 3 Scenarios That Sent Us Back to Core Data

Summary

Production experience with SwiftData on iOS 17+. Migration pitfalls coming from Core Data, the practical realities of concurrency, real performance baseline numbers, and why we went back to Core Data on some projects.

  • Across six months in production, SwiftData was rolled back to Core Data in some scenarios.
  • In an e-commerce app with 1.5M+ records, complex #Predicate queries ran 25-40% slower than Core Data.
  • Because models inside a @ModelActor aren't Sendable, results have to be re-fetched via PersistentIdentifier.
  • In a 100K-row benchmark SwiftData is 10-15% slower than Core Data, and there is no NSPersistentHistoryToken equivalent.
SwiftData in Production: 6 Months of Real-World Experience and 3 Scenarios That Sent Us Back to Core Data

# SwiftData in Production: 6 Months of Real-World Experience and 3 Scenarios That Sent Us Back to Core Data

SwiftData arrived with iOS 17, and at WWDC23 Apple presented it as "the modern successor to Core Data." After six months in production, here is the verdict: SwiftData is genuinely ergonomic, but it cannot fully replace Core Data. On some projects we went back. In this article we cover which ones, why, and the decision matrix to use when weighing the move.

Pro Tip: Before you start a SwiftData migration, leave your Core Data store unrenamed. SwiftData keeps the name 'Default.store', and if you ever need to roll back, the original name is critical.

Table of Contents

  1. What SwiftData Is — and What It Isn't
  2. Core Data → SwiftData Migration Strategy
  3. The @Model Macro: Inner Mechanics
  4. Querying: @Query vs FetchDescriptor vs the Predicate DSL
  5. Concurrency: @ModelActor and Its Pitfalls
  6. Performance Baseline — Real Numbers
  7. iCloud Sync: SwiftData CloudKit Integration
  8. 3 Scenarios That Sent Us Back to Core Data
  9. Migration Decision Matrix
  10. The 2026-2027 SwiftData Roadmap

1. What SwiftData Is — and What It Isn't

SwiftData is a Swift-native abstraction layer built on top of Core Data. Let's be explicit:

  • Not a new database engine — it uses SQLite + the Core Data store
  • Pure-Swift API — no Objective-C interop
  • Macro-driven @Model, @Query, and @Attribute do their work at compile time
  • Native to modern concurrency async/await first
  • Type-safe predicates — the #Predicate macro killed off KVC strings

What it can't do:

  • You can't use a custom NSManagedObject subclass
  • No programmatic model migration (lightweight only, automatic)
  • NSPersistentHistoryToken — change tracking isn't at the same level
  • No cross-store fetch (Core Data multi-context)
  • NO backwards compatibility before iOS 17

2. Core Data → SwiftData Migration Strategy

We had three different scenarios, each with its own strategy:

Scenario A — Greenfield iOS 17+ project (TahminApp v3):

swift
1// Direkt SwiftData
2@Model
3final class Prediction {
4 var id: UUID
5 var title: String
6 var createdAt: Date
7 
8 init(id: UUID = .init(), title: String, createdAt: Date = .now) {
9 self.id = id
10 self.title = title
11 self.createdAt = createdAt
12 }
13}
14 
15let container = try ModelContainer(for: Prediction.self)

Scenario B — iOS 17+ minimum, existing Core Data app (ESPPoint v4):

Convert the Core Data .xcdatamodeld file into a SwiftData mapping. Apple offers the Schema(versionedSchema:) and ValueTransformer APIs for this pattern:

swift
1let container = try ModelContainer(
2 for: Schema([Device.self, Reading.self]),
3 migrationPlan: ESPPointMigrationPlan.self
4)

Scenario C — iOS 15+ support required (MADPAW):

Stay on Core Data. SwiftData requires iOS 17 as a minimum, and you can't afford to lose your existing users.


3. The `@Model` Macro: Inner Mechanics

The @Model Swift macro derives an NSManagedObject subclass at compile time. Inspection:

swift
1@Model
2final class User {
3 var id: UUID
4 var email: String
5 @Attribute(.unique) var username: String
6 var posts: [Post] = []
7 
8 init(id: UUID = .init(), email: String, username: String) {
9 self.id = id
10 self.email = email
11 self.username = username
12 }
13}

Macro expansion:

  • User final class → inherits from NSManagedObject
  • var id@NSManaged getter/setter + KVC keypath
  • @Attribute(.unique) → store-level unique constraint
  • var posts → to-many relationship + automatic inverse

Performance cost: Compile time +5-8s per 10 models. Build cache is effective. Runtime overhead is nil.

Pro Tip: @Attribute(.transformable(by: ...)) is the equivalent of Core Data's transformable attribute. It's critical for custom Codable types. JSON-style data isn't supported by default in SwiftData.

4. Querying: `@Query` vs `FetchDescriptor` vs the Predicate DSL

There are three query APIs, each with its own place:

1. `@Query` — SwiftUI view-level:

swift
1struct PostListView: View {
2 @Query(filter: #Predicate<Post> { $0.isPublished == true },
3 sort: \.createdAt, order: .reverse)
4 var posts: [Post]
5 
6 var body: some View {
7 List(posts) { post in PostRow(post: post) }
8 }
9}

2. `FetchDescriptor` — programmatic:

swift
1let descriptor = FetchDescriptor<Post>(
2 predicate: #Predicate { $0.authorID == userID },
3 sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
4)
5descriptor.fetchLimit = 50
6descriptor.propertiesToFetch = [\.title, \.excerpt] // partial fetch
7 
8let posts = try modelContext.fetch(descriptor)

3. The `#Predicate` macro — type-safe filtering:

swift
1// Eski Core Data: NSPredicate(format: "title CONTAINS[cd] %@ AND year > %d", search, 2023)
2// SwiftData: type-safe
3let pred = #Predicate<Post> { post in
4 post.title.localizedStandardContains(search) && post.year > 2023
5}

Compile-time errors, no string typos, autocomplete.

Gotcha: #Predicate supports Foundation closures (.contains, .starts(with:), etc.) but not every Swift function. Computed properties can't be used — you need a @Transient mark or a stored property.

5. Concurrency: `@ModelActor` and Its Pitfalls

SwiftData aims for full compatibility with Swift 6 strict concurrency. @ModelActor is the new primitive:

swift
1@ModelActor
2actor BackgroundDataProcessor {
3 func batchInsert(_ items: [ItemDTO]) async {
4 for item in items {
5 modelContext.insert(Item(dto: item))
6 }
7 try? modelContext.save()
8 }
9}
10 
11// Kullanım — main'den uzakta
12let processor = BackgroundDataProcessor(modelContainer: container)
13await processor.batchInsert(largeDataset)

Pitfall 1: @ModelActor actor isolation is strict — you can't copy models out of the actor. The result either has to be a Sendable struct, or it has to be re-fetched via its PersistentID:

swift
1// ❌ Yanlış — Item Sendable değil (NSManagedObject)
2let items = await processor.fetchAll() // hata
3 
4// ✅ Doğru — ID return + main context'te re-fetch
5let ids: [PersistentIdentifier] = await processor.fetchAllIDs()
6let mainContext = container.mainContext
7let items = ids.compactMap { mainContext.model(for: $0) as? Item }

Pitfall 2: A save on a background context doesn't automatically invalidate the main context. Listen with NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave), or drive a reactive UI with @Observable.


6. Performance Baseline — Real Numbers

On an A17 Pro / iOS 18, with a 100K row Post entity:

Operation
Core Data
SwiftData
Delta
Insert (batch 1000)
240ms
280ms
+17%
Fetch all + filter
180ms
195ms
+8%
Fetch + sort + limit 50
45ms
52ms
+15%
Save context
35ms
38ms
+9%
App cold launch (DB load)
280ms
310ms
+11%

Interpretation: SwiftData is roughly 10-15% slower than Core Data, because of macro overhead and NSManagedObject wrapping. For most apps this is negligible. It can matter on data-heavy (1M+ row) projects.

Memory: SwiftData containers hold about 15-20MB more baseline. iOS 18 closed part of that gap.


7. iCloud Sync: SwiftData CloudKit Integration

SwiftData + CloudKit:

swift
1let modelConfiguration = ModelConfiguration(
2 schema: Schema([Item.self]),
3 cloudKitDatabase: .private("iCloud.com.muhittincamdali.app")
4)
5let container = try ModelContainer(
6 for: Item.self,
7 configurations: modelConfiguration
8)

Automatic sync: Inserts, updates and deletes are pushed to CloudKit. Conflict resolution defaults to last-writer-wins.

Pitfalls:

  • Schema changes have to be deployed manually from the CloudKit dashboard
  • The CloudKit record_type has to match the SwiftData entity exactly
  • SwiftData doesn't support the public database yet — private + shared only
  • The initial sync doesn't show the user a "Loading from iCloud..." spinner — build your own UX

8. 3 Scenarios That Sent Us Back to Core Data

Over six months we moved three projects back from SwiftData to Core Data. The reasons:

Scenario 1: 1.5M+ rows, complex queries

An e-commerce inventory app with 1.5M products and multi-level filtering (category × brand × price × stock × tag). On complex compound conditions, SwiftData's #Predicate ran 25-40% slower than Core Data. For this app that was unacceptable.

Scenario 2: Custom NSManagedObject behavior

A banking app needed audit logging, validation hooks (willChangeValue) and a custom description override. SwiftData's @Model blocks all of these. After the migration we needed override subclasses in 47 places — trivial in Core Data.

Scenario 3: Persistent History Tracking

A cross-device, offline-first app tracks sync state with NSPersistentHistoryToken. SwiftData offers no comparable API — in particular there's no equivalent of fetchHistory(after: token). Implementing it by hand takes 200+ lines.

Pro Tip: Before putting SwiftData into production, evaluate your data scale and your special requirements. <100K rows + standard CRUD → SwiftData. 1M+ rows or custom behavior → Core Data.

9. Migration Decision Matrix

Criterion
SwiftData
Core Data
iOS 17+ exclusive is OK
iOS 15-16 support required
<100K rows
1M+ rows
⚠️ measure
Custom NSManagedObject behavior
CloudKit private DB sync
Persistent history tracking
Type-safe predicates
❌ (string-based)
Native SwiftUI binding
⚠️ wrapper
Migration backwards compat
⚠️
Programmatic model
Native async/await
✅ (iOS 15+)

Rule: If there are 3+ red boxes on the Core Data side → take the SwiftData route.

If there are 3+ red boxes on the SwiftData side → Core Data is the safer bet.


10. The 2026-2027 SwiftData Roadmap

Signals picked up at WWDC25:

  • Q3 2026: Custom model migration API (programmatic plan)
  • Q4 2026: Public CloudKit database support
  • Q1 2027: NSPersistentHistoryToken-equivalent change tracking
  • Q2 2027: Index hint API (compound + ordered index)
  • Q3 2027: Sequence sync (offline queue with conflict UI)
  • Q4 2027: SwiftData 2.0 — native schema versioning

Direction of travel: Apple is continuing to invest in SwiftData. Within 18-24 months, all of Core Data's strengths could be present in SwiftData. The gap is closing right now, but it isn't closed.


Conclusion

SwiftData is the right call for iOS 17+ projects — modern Swift, type-safe predicates, SwiftUI bindings and less boilerplate. But for scenarios that involve 1M+ rows of data, custom NSManagedObject behavior or persistent history, Core Data still wins.

The pragmatic decision: new project + iOS 17+ minimum + standard CRUD → SwiftData. Existing Core Data + working production → run it through the decision matrix before migrating. If there are 3+ blockers, stay where you are.

Once you've cleared the decision matrix, budget 2-4 weeks for the migration and 1 week for the schema migration. Watch production crashes closely for 2-3 weeks — making sure existing users lose no data during the migration is critical.

Related resources:

Tags

#SwiftData#Core Data#Persistence#iOS 17#Production#Migration#Concurrency
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share