SwiftData Persist, query, and manage structured data in iOS 26+ apps using SwiftData with Swift 6.2. Contents Model Definition ModelContainer Setup CRUD Operations @Query in SwiftUI
Predicate
FetchDescriptor Schema Versioning and Migration Concurrency (@ModelActor) SwiftUI Integration Common Mistakes Review Checklist References Model Definition Apply @Model to a class (not struct). Generates PersistentModel , Observable , Sendable . @Model class Trip { var name : String var destination : String var startDate : Date var endDate : Date var isFavorite : Bool = false @Attribute ( . externalStorage ) var imageData : Data ? @Relationship ( deleteRule : . cascade , inverse : \ LivingAccommodation . trip ) var accommodation : LivingAccommodation ? @Transient var isSelected : Bool = false // Always provide default init ( name : String , destination : String , startDate : Date , endDate : Date ) { self . name = name ; self . destination = destination self . startDate = startDate ; self . endDate = endDate } } @Attribute options : .externalStorage , .unique , .spotlight , .allowsCloudEncryption , .preserveValueOnDeletion (iOS 18+), .ephemeral , .transformable(by:) . Rename: @Attribute(originalName: "old_name") . @Relationship : deleteRule: .cascade / .nullify (default)/ .deny / .noAction . Specify inverse: for reliable behavior. Unidirectional (iOS 18+): inverse: nil .
Unique (iOS 18+)
:
Unique([.firstName, .lastName])
-- compound uniqueness. Inheritance (iOS 26+) : @Model class BusinessTrip: Trip { var company: String } . Supported types: Bool , Int / UInt variants, Float , Double , String , Date , Data , URL , UUID , Decimal , Array , Dictionary , Set , Codable enums, Codable structs (composite, iOS 18+), relationships to @Model classes. ModelContainer Setup // Basic let container = try ModelContainer ( for : Trip . self , LivingAccommodation . self ) // Configured let config = ModelConfiguration ( "Store" , isStoredInMemoryOnly : false , groupContainer : . identifier ( "group.com.example.app" ) , cloudKitDatabase : . private ( "iCloud.com.example.app" ) ) let container = try ModelContainer ( for : Trip . self , configurations : config ) // With migration plan let container = try ModelContainer ( for : SchemaV2 . Trip . self , migrationPlan : TripMigrationPlan . self ) // In-memory (previews/tests) let container = try ModelContainer ( for : Trip . self , configurations : ModelConfiguration ( isStoredInMemoryOnly : true ) ) CRUD Operations // CREATE let trip = Trip ( name : "Summer" , destination : "Paris" , startDate : . now , endDate : . now + 86400 * 7 ) modelContext . insert ( trip ) try modelContext . save ( ) // or rely on autosave // READ let trips = try modelContext . fetch ( FetchDescriptor < Trip
( predicate :
Predicate
{ $0 . destination == "Paris" } , sortBy : [ SortDescriptor ( \ . startDate ) ] ) ) // UPDATE -- modify properties directly; autosave handles persistence trip . destination = "Rome" // DELETE modelContext . delete ( trip ) try modelContext . delete ( model : Trip . self , where :
Predicate
{ $0 . isFavorite == false } ) // TRANSACTION (atomic) try modelContext . transaction { modelContext . insert ( trip ) ; trip . isFavorite = true } @Query in SwiftUI struct TripListView : View { @Query ( filter :
Predicate
< Trip
{ $0 . isFavorite == true } , sort : \ . startDate , order : . reverse ) private var favorites : [ Trip ] var body : some View { List ( favorites ) { trip in Text ( trip . name ) } } } // Dynamic query via init struct SearchView : View { @Query private var trips : [ Trip ] init ( search : String ) { _trips = Query ( filter :
Predicate
< Trip
{ trip in search . isEmpty || trip . name . localizedStandardContains ( search ) } , sort : [ SortDescriptor ( \ . name ) ] ) } var body : some View { List ( trips ) { trip in Text ( trip . name ) } } } // FetchDescriptor query struct RecentView : View { static var desc : FetchDescriptor < Trip
{ var d = FetchDescriptor < Trip
( sortBy : [ SortDescriptor ( \ . startDate ) ] ) d . fetchLimit = 5 ; return d } @Query ( RecentView . desc ) private var recent : [ Trip ] var body : some View { List ( recent ) { trip in Text ( trip . name ) } } }
Predicate
Predicate
< Trip
{ $0 . destination . localizedStandardContains ( "paris" ) } // String
Predicate
< Trip
{ $0 . startDate
Date . now } // Date
Predicate
< Trip
{ $0 . isFavorite && $0 . destination != "Unknown" } // Compound
Predicate
< Trip
{ $0 . accommodation ? . name != nil } // Optional
Predicate
- <
- Trip
- >
- {
- $0
- .
- tags
- .
- contains
- {
- $0
- .
- name
- ==
- "adventure"
- }
- }
- // Collection
- Supported:
- ==
- ,
- !=
- ,
- <
- ,
- <=
- ,
- >
- ,
- >=
- ,
- &&
- ,
- ||
- ,
- !
- ,
- contains()
- ,
- allSatisfy()
- ,
- filter()
- ,
- starts(with:)
- ,
- localizedStandardContains()
- ,
- caseInsensitiveCompare()
- , arithmetic, ternary, optional chaining, nil coalescing, type casting.
- Not supported
- flow control, nested declarations, arbitrary method calls.
FetchDescriptor
var
d
=
FetchDescriptor
<
Trip
( predicate : ... , sortBy : [ ... ] ) d . fetchLimit = 20 ; d . fetchOffset = 0 d . includePendingChanges = true d . propertiesToFetch = [ \ . name , \ . startDate ] d . relationshipKeyPathsForPrefetching = [ \ . accommodation ] let trips = try modelContext . fetch ( d ) let count = try modelContext . fetchCount ( d ) let ids = try modelContext . fetchIdentifiers ( d ) try modelContext . enumerate ( d , batchSize : 1000 ) { trip in trip . isProcessed = true } Schema Versioning and Migration enum SchemaV1 : VersionedSchema { static var versionIdentifier = Schema . Version ( 1 , 0 , 0 ) static var models : [ any PersistentModel . Type ] { [ Trip . self ] } @Model class Trip { var name : String ; init ( name : String ) { self . name = name } } } enum SchemaV2 : VersionedSchema { static var versionIdentifier = Schema . Version ( 2 , 0 , 0 ) static var models : [ any PersistentModel . Type ] { [ Trip . self ] } @Model class Trip { var name : String ; var startDate : Date ? // New property init ( name : String ) { self . name = name } } } enum TripMigrationPlan : SchemaMigrationPlan { static var schemas : [ any VersionedSchema . Type ] { [ SchemaV1 . self , SchemaV2 . self ] } static var stages : [ MigrationStage ] { [ migrateV1toV2 ] } static let migrateV1toV2 = MigrationStage . lightweight ( fromVersion : SchemaV1 . self , toVersion : SchemaV2 . self ) } // Custom migration for data transformation static let migrateV2toV3 = MigrationStage . custom ( fromVersion : SchemaV2 . self , toVersion : SchemaV3 . self , willMigrate : nil , didMigrate : { context in let trips = try context . fetch ( FetchDescriptor < SchemaV3 . Trip
( ) ) for trip in trips { trip . displayName = trip . name . capitalized } try context . save ( ) } ) Lightweight handles: adding optional/defaulted properties, renaming ( originalName ), removing properties, adding model types. Concurrency (@ModelActor) @ModelActor actor DataHandler { func importTrips ( _ records : [ TripRecord ] ) throws { for r in records { modelContext . insert ( Trip ( name : r . name , destination : r . dest , startDate : r . start , endDate : r . end ) ) } try modelContext . save ( ) // Always save explicitly in @ModelActor } func process ( tripID : PersistentIdentifier ) throws { guard let trip = self [ tripID , as : Trip . self ] else { return } trip . isProcessed = true ; try modelContext . save ( ) } } let handler = DataHandler ( modelContainer : container ) try await handler . importTrips ( records ) Rules : ModelContainer is Sendable . ModelContext is NOT -- use on its creating actor. Pass PersistentIdentifier (Sendable) across boundaries. Never pass @Model objects across actors. SwiftUI Integration @main struct MyApp : App { var body : some Scene { WindowGroup { ContentView ( ) } . modelContainer ( for : [ Trip . self , LivingAccommodation . self ] ) } } struct DetailView : View { @Environment ( \ . modelContext ) private var modelContext let trip : Trip var body : some View { Text ( trip . name ) Button ( "Delete" ) { modelContext . delete ( trip ) } } }
Preview
{
let
config
=
ModelConfiguration
(
isStoredInMemoryOnly
:
true
)
let
container
=
try
!
ModelContainer
(
for
:
Trip
.
self
,
configurations
:
config
)
container
.
mainContext
.
insert
(
Trip
(
name
:
"Preview"
,
destination
:
"London"
,
startDate
:
.
now
,
endDate
:
.
now
+
86400
)
)
return
TripListView
(
)
.
modelContainer
(
container
)
}
Common Mistakes
1. @Model on struct
-- Use class.
@Model
requires reference semantics.
2. @Transient without default
-- Always provide default:
@Transient var x: Bool = false
.
3. Missing .modelContainer
-- @Query returns empty without a container on the view hierarchy.
4. Passing model objects across actors:
// WRONG: await handler.process(trip: trip)
// CORRECT: await handler.process(tripID: trip.persistentModelID)
5. ModelContext on wrong actor:
// WRONG: Task.detached { context.fetch(...) }
// CORRECT: Use @ModelActor for background work
6. Unsupported #Predicate expressions:
// WRONG: #Predicate
Predicate
uses only supported operators Background work uses @ModelActor PersistentIdentifier used across actor boundaries Schema changes have VersionedSchema + SchemaMigrationPlan Large data uses @Attribute(.externalStorage) CloudKit models use optionals and avoid unique constraints Explicit save() in @ModelActor methods Previews use ModelConfiguration(isStoredInMemoryOnly: true) @Model classes accessed from SwiftUI views are on @MainActor via @ModelActor or MainActor isolation References See references/swiftdata-advanced.md for custom data stores, history tracking, CloudKit, Core Data coexistence, composite attributes, model inheritance, undo/redo, and performance patterns. See references/swiftdata-queries.md for @Query variants, FetchDescriptor deep dive, sectioned queries, dynamic queries, and background fetch patterns. See references/core-data-coexistence.md for standalone Core Data patterns and Core Data to SwiftData migration strategies.