WWDC Quick Look 💓 By SwiftGGTeam
Model your schema with SwiftData

Model your schema with SwiftData

Watch original video

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 independentVersionedSchemaenumeration -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 versionVersionedSchema, 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: DefinitionSchemaV1, even if the content is simple. Subsequent versions are numbered incrementally, and the migration plan is updated with each change.

Comments

GitHub Issues · utterances