Highlight
SwiftData passes
@Model、@Attribute、@RelationshipMacros make data modeling completely code-based. Developers can directly define database schema, configure persistence behavior, and manage version migration using Swift code without any external file format.
Core Content
From Core Data to SwiftData: A change in modeling approach
In the past, Core Data was used for modeling, and it needed to be opened..xcdatamodeldfile, add Entity, configure Attribute, and set Relationship in the graphical interface. The model and code are separated, and changing one part requires synchronizing the other part, which is prone to errors.
SwiftData brings modeling completely into code. Add to a Swift class@Modelmacro, it automatically gains persistence capabilities.
(00:56)@ModelHongrangTripclasses automatically followPersistentModelThe protocol,framework generates a complete Schema based on attribute,definitions. Code is truth, and model definitions are the single source of Schema.
@Attribute: Finely control attribute behavior
(01:50) After publishing the application, it was found that Trip with the same name caused a conflict.@Attribute(.unique)It can ensure that the attribute value is unique and execute upsert semantics when saving. want tostart_dateChange tostartDate, but changing the name directly will be regarded as a new attribute, resulting in data loss.@AttributeoforiginalNameParameters can map old and new attribute names to avoid data loss and achieve smooth migration.
Detailed Content
@Attribute: unique constraints and upsert
(01:50) After publishing the application, it was found that Trip with the same name caused a conflict. use@Attribute(.unique)You can ensure that attribute values are unique:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
Key Points:
-@Attribute(.unique)make surenameField value is unique
- If a Trip with the same name exists when saving, execute upsert (try inserting first, update if there is a conflict)
- Unique constraints apply to basic types (number, string, UUID) and one-to-one relationships
@Attribute: attribute rename mapping
(02:48) I want tostart_dateChange tostartDate, but directly changing the name will be regarded as a new attribute, resulting in data loss. useoriginalNameParameter mapping:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
Key Points:
-originalNameMap new attribute names to old column names in the database
- Avoid data loss and achieve smooth migration
-
@AttributeAlso supports.externalStorage(Big data stored in external files) and transformable types
@Relationship: Manage object relationships
Cascade Delete
(04:00) The default deletion rule is nullify - when a Trip is deleted, the foreign keys of the associated bucketList and livingAccommodation are set to nil, becoming orphan data. If you want to delete a Trip, delete the associated data as well:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
@Relationship(.cascade)
var bucketList: [BucketListItem]? = []
@Relationship(.cascade)
var livingAccommodation: LivingAccommodation?
}
Key Points:
-@Relationship(.cascade)Automatically delete all child objects when deleting the parent object
- Implicit reverse relationships are automatically discovered, no additional annotations are required
-
@RelationshipAlso supportsoriginalNameand setting the minimum/maximum number of to-many relationships
@Transient: Exclude attributes that do not require persistence
(04:54) You want to record the number of times a Trip has been viewed, but do not need to persist:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
@Relationship(.cascade)
var bucketList: [BucketListItem]? = []
@Relationship(.cascade)
var livingAccommodation: LivingAccommodation?
@Transient
var tripViews: Int = 0
}
Key Points:
-@TransientMarked attributes will not be saved in the database
- Default values must be provided to ensure reasonable initial values when getting objects from SwiftData
- Suitable for caching, temporary calculation results and other data that do not require persistence
Schema migration: the correct way to evolve versions
(07:12) After multiple versions of the application are released, the Schema needs to evolve. SwiftData usesVersionedSchemaDefine different versions of Schema:
enum SampleTripsSchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
enum SampleTripsSchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
enum SampleTripsSchemaV3: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
Key Points:
- Each version is an independent
VersionedSchemaenumeration -modelsProperty lists all model types included in this version - The version class defines the version internally
@Modelkind
(07:49) Define the migration plan:
enum SampleTripsMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SampleTripsSchemaV1.self,
toVersion: SampleTripsSchemaV2.self,
willMigrate: { context in
let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
// De-duplicate Trip instances here...
try? context.save()
}, didMigrate: nil
)
static let migrateV2toV3 = MigrationStage.lightweight(
fromVersion: SampleTripsSchemaV2.self,
toVersion: SampleTripsSchemaV3.self
)
}
Key Points:
-schemasList all versions, sorted by order
-stagesDefine migration steps between every two versions
-.customMigration needs to be written manuallywillMigrateClosures handle data conversion
-.lightweightMigrations are handled automatically by the framework (like property renaming)
-willMigrateExecuted before Schema changes, old version data can be queried for preprocessing
-didMigrateExecuted after Schema changes, you can verify the migration results
(08:40) Configure the migration plan when the app starts:
struct TripsApp: App {
let container = ModelContainer(
for: Trip.self,
migrationPlan: SampleTripsMigrationPlan.self
)
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Key Points:
-ModelContainerPassed in during initializationmigrationPlan- The framework automatically detects the current database version and performs required migrations in order
- The migration is executed in a background thread, and the application starts normally after completion
Core Takeaways
Simplify data synchronization with unique constraints
- What to do: Add fields with business uniqueness (such as user name, order number)
@Attribute(.unique)- Why is it worth doing: upsert semantics makes synchronization logic simpler, and there is no need to query first and then decide whether to insert or update. - How to start: In
@ModelAdd key fields to the class@Attribute(.unique)
Enable external storage for big data properties
- What to do: Use of binary data such as images and audio
@Attribute(.externalStorage)- Why is it worth doing: Reduce database file size, improve query performance, and make big data access more efficient - How to start: Mark images and other attributes as
@Attribute(.externalStorage), SwiftData automatically manages file storage
Design a reasonable deletion cascade strategy
- What to do: Clarify the deletion behavior of each parent-child relationship, use
@Relationship(.cascade)or.nullify- Why it’s worth doing: Avoid orphan data and maintain data integrity - How to start: Analyze the relationships in the data model and add relationships that require cascading deletion
@Relationship(.cascade)
Establish versioned schemas and migration plans early
- What to do: Used since the first version
VersionedSchema, even if migration is not currently required - Why it’s worth it: Late migration planning is much more difficult than planning it from the beginning
- How to start: Definition
SchemaV1, even if the content is simple. Subsequent versions are numbered incrementally, and the migration plan is updated with each change.
Related Sessions
- Meet SwiftData — Getting started with the SwiftData framework
- Build an app with SwiftData — A practical demonstration of building a complete application with SwiftData
- Dive deeper into SwiftData — SwiftData advanced query and performance optimization
Comments
GitHub Issues · utterances