Learn how to use schema macros and migration plans with SwiftData to build more complex features for your app. We'll show you how to fine-tune your persistence with @Attribute and @Relationship options. Learn how to exclude properties from your data model with @Transient and migrate from one version of your schema to the next with ease.
To get the most out of this session, we recommend first watching "Meet SwiftData" and "Build an app with SwiftData" from WWDC23.
♪ ♪ Rishi: Hello, my name is Rishi Verma, and this session covers how to code your models to build a schema for SwiftData. I'll begin by covering how you can utilize schema macros to their fullest potential and how you can evolve your schema with schema migrations as your app changes. Before getting started, please watch "Meet SwiftData" and "Build an app with SwiftData," as this content will develop upon the concepts introduced in those videos. SwiftData is a powerful framework for data modeling and management and enhances your modern Swift app. Like SwiftUI, it focuses entirely on code, with no external file formats, and uses Swift's new macro system to create a seamless API experience.
I am working on the SampleTrips app, which allows users to plan out some upcoming trips. Each trip is to be created with a name, a destination, as well as start and end dates. A trip can also contain relationships for bucket list items and where the travelers will stay. Adding SwiftData is as simple as adding the import and decorating the Trip with @Model. That's it. The @Model macro conforms my Trip class to PersistentModel and generates a descriptive schema. The code defining my model is now the source of truth for my application's schema. The default behavior of my trip model is good, but I think I can fine-tune it a little. SwiftData's schema macros allow me to customize the behavior of the persistence experience to work perfectly for my app. When I published my app with the original schema, I did not ensure each trip name was unique. This caused a few conflicts between trips with the same name that I now need to resolve. This can be fixed with the @Attribute schema macro and using the unique option. SwiftData will generate a schema for my trip's model that now ensures any trip that I save to the persistent back end will have a unique name. If a trip already exists with that name, then the persistent back end will update to the latest values. This is called an upsert. An upsert starts as an insert. If the insert collides with existing data, it becomes an update and updates the properties of the existing data. I can apply unique constraints on other properties as well, so long as they are primitive value types such as numerics, string, or UUID, or I can even decorate a to-one relationship. My schema needs a bit more work. I want to remove these pesky underscores from my start_date and end_date that I originally specified. If I just rename the variables, this would be seen as a new property in my generated schema. I don't want SwiftData to create these new properties. Instead, I want to preserve the existing data as is. I can do so by simply mapping the original name to the property name using @Attribute and specifying the originalName: parameter. By mapping these from the original name, I can avoid data loss. This also ensures my schema update will be a simple migration for the next release of the SampleTrips app. And the @Attribute macro can do so much more, including store large data externally and provide support for transformables.
My trips are shaping up nicely, but now I want to work on the relationships. When I add a new bucket list item or a living accommodation to my trip, SwiftData will implicitly discover the inverses between my models and set them for me. The implicit inverses do not require any annotations. They just work. Implicit inverses use a default delete rule that will nullify the bucket list items and living accommodation properties when a trip is deleted. However, I want my bucket list items and living accommodation to be deleted along with the trip. I can easily do that by adding the @Relationship macro with a cascade delete rule. Now when I delete my trip, it will delete those relationships as well. And the @Relationship macro does so much more, including the originalName modifier and the ability to specify the minimum and maximum count on a to-many relationship. The SampleTrips app is shaping up nicely, but I still have an update to do. Now, I want to add a way to track how many times I view a trip. This way I can gauge how excited I am about taking a vacation. I can't wait! I do not want this view count to be persisted by SwiftData, however, and I can easily do that with the @Transient macro. I simply decorate my property with @Transient, and this particular property will not be persisted. It's just that easy. The @Transient macro helps you avoid persisting unnecessary data. Make sure you provide a default value for transient properties. This ensures they have logical values when fetched from SwiftData. For more information on utilizing these schema macros, check out the SwiftData documentation. The SampleTrips app schema has gone through several evolutions as I tailored the persistence experience. I need to ensure that my app can handle those updates from release to release. And when you make a change to your schema, like adding or removing a property, a data migration occurs. These migrations can be tricky scenarios, but SwiftData makes it easy. VersionedSchema and SchemaMigrationPlan are here to help you with that. Whenever you prepare to release a new version of your app with changes to your SwiftData models, define a VersionedSchema that encapsulates your previously released schema. Each distinct version of your schema should be defined as a VersionedSchema so that SwiftData knows what changes occurred between them. Then, use your total ordering of VersionedSchemas to create a SchemaMigrationPlan. This will allow SwiftData to perform the needed migrations in order. Once you've laid out your ordered schemas in the migration plan, you can begin to define each migration stage. There are two different types of migration stages available to you. The first is a lightweight migration stage. Lightweight migrations do not require any additional code to migrate the existing data for my next app release. Modifications like adding originalName to my date properties or specifying the delete rules on my relationships are lightweight migration eligible. However, making the name of a trip unique is not eligible for a lightweight migration. I need to create a custom migration stage for this change, in which I can deduplicate my trips, before their names are made unique. I start by taking the original schema from my first release and encapsulating it in a VersionedSchema. I name this versioned schema SampleTripsSchemaV1. Each of my versioned schemas list the model classes they define. Version 2 of my schema is where I added the uniqueness constraint on trip names. I create another versioned schema that also encapsulates the changes I made to the Trip model class. I do the same for version 3 of my schema, capturing the name changes made to start and end date. Now that I have all of my VersionedSchemas, I construct a SchemaMigrationPlan to describe how to handle the migrations from release to release. It's rather simple. I just provide the total ordering of my application's schemas. Then, I need to annotate which migrations are lightweight or custom. For V1 to V2, I need a custom stage where I can perform an operation before the data is migrated. In the willMigrate closure, I can deduplicate my trips before the migration happens. SwiftData will detect when a migration from V1 to V2 will occur and will perform this closure for me. The other migration for originalName is lightweight, so I can add that stage in as well. Now that I've defined all of the details about my migration plans, it's time to perform the migrations. I setup my ModelContainer with my current schema and the migration plan, and I'm done. My users can upgrade from any version to the latest release, and I have ensured the data will be preserved. I can't wait to use the SampleTrips app to plan my upcoming vacation. Harness schema macros to convey additional metadata for your schema, and as your application evolves, capture those evolutions in a VersionedSchema so your app can migrate from any previous release. Check out these other talks, and we look forward to seeing the amazing things you all make with SwiftData. It has been an honor.
@Attribute(.unique) var name: Stringvar destination: String@Attribute(originalName: "start_date") var startDate: Date@Attribute(originalName: "end_date") var endDate: Date@Relationship(.cascade)
var bucketList: [BucketListItem]?= 
var livingAccommodation: LivingAccommodation?