Streaming is available in most browsers,
and in the Developer app.
-
Dive deeper into SwiftData
Learn how you can harness the power of SwiftData in your app. Find out how ModelContext and ModelContainer work together to persist your app's data. We'll show you how to track and make your changes manually and use SwiftData at scale with FetchDescriptor, SortDescriptor, and enumerate. To get the most out of this session, we recommend first watching "Meet SwiftData" and "Model your schema with SwiftData" from WWDC23.
Chapters
- 0:00 - Intro
- 3:42 - Configuring persistence
- 7:21 - Track and persist changes
- 11:20 - Modeling at scale
- 14:54 - Wrap-up
Ressourcen
Related Videos
WWDC23
-
Download
♪ ♪ Nick Gillett: Hi, I'm Nick Gillett, an Engineer here at Apple on the SwiftData team. In this session, I'll examine in detail how applications built with SwiftData evolve to take advantage of this rich, powerful new framework. First, I'll examine how to configure persistence in an application. Next, I'll talk about how to use the ModelContext to track and persist changes. Finally, I'll examine how to get the most out of SwiftData when working with objects in your code. I'd like to note that this session builds on the concepts and APIs introduced in "Meet Swift Data" and "Model your Schema with SwiftData". I highly recommend reviewing those sessions before continuing with this one. In this talk, I'll be referencing a new sample application, SampleTrips, that we built this year to demonstrate how easy it is to build applications with SwiftData. SampleTrips makes it easy to organize my ideas about where and when I want to travel. SwiftData also makes it easy to implement standard platform practices like undo and automatically saving when a user switches applications. SwiftData is a new way of persisting data in applications that use Swift. It's designed to work with the types you're already using in your code like Classes and Structs. At the core of this concept is the Model, described by a new macro, @Model, which tells SwiftData about the types you want to persist. This is the Trip class from the SampleTrips application. It has a few properties to capture information about a trip and some references to other objects used in SampleTrips. We designed SwiftData to provide a minimal distance between the code you would normally write without persistence, as I have here, and the code you have to write with persistence. With just a few changes, I've told SwiftData this Trip is a Model I want to persist and described how its relationships to the BucketListItem and LivingAccommodations should behave. Where possible, SwiftData automatically infers the structure you want to use from the code you write. But SwiftData also offers a powerful set of customizations to help you describe exactly how you want your data to be stored. You can learn all about the power of Model in "Model your Schema with SwiftData". These annotations to the Trip class enable it to play two important roles in SwiftData. The first is to describe the object graph of the application, called the Schema, and the second is that the Trip class will be an interface that I can write code against. This duality, the ability to play both parts, makes classes annotated with the @Model macro the central point of contact in applications that use SwiftData, and there is an aligned API concept to support each of these roles.
The Schema is applied to a class called the ModelContainer to describe how data should be persisted.
The ModelContainer consumes the Schema to generate a database that can hold instances of the Model classes.
When working with instances of a Model class in code, those instances are linked to a ModelContext which tracks and manages their state in memory. This duality is at the core of SwiftData and in this section, I'll take a detailed look at the first role of the model, to describe the structure of persistence and how that works with the ModelContainer. The ModelContainer is where you describe how data is stored, or persisted, on a device. We can think of the ModelContainer as the bridge between the Schema and its persistence. It's where descriptions about how objects are stored, like whether they're in memory, or on disk, meet the operational and evolutionary semantics of that storage like versioning, migration, and graph separation. Instantiating a container with a Schema is easy. I can provide just the type I want to work with and SwiftData will figure out the rest of the Schema for me. For example, because the Trip class is related to other model types, ModelContainer actually infers this Schema, even though I only passed it a single type. ModelContainer has a number of other powerful initializers that are designed to grow with your code to enable increasingly complex configurations using a class called the ModelConfiguration.
The ModelConfiguration describes the persistence of a Schema. It controls where data is stored, like in memory for transient data or on disk for persistent data. ModelConfiguration can use a specific file URL chosen by you, or it can generate one automatically using the entitlements of your application like the group container entitlement. The configuration can also describe that a persistence file should be loaded in a read only mode, preventing writes to sensitive or template data. And finally, applications that use more than one CloudKit container can specify it as part of the ModelConfiguration for a Schema.
Let's imagine I want to add some contact information to SampleTrips using new Person and Address classes. First, the total Schema is declared containing all of the types I want to use. Next, a Configuration is declared for the SampleTrips data containing the Trip, BucketListItem, and LivingAccommodations models. It declares a URL for the file I want to use to store this specific object graph's data, and a container identifier for the CloudKit container I want to use when syncing the SampleTrips data to CloudKit. Then, the models for the new Schema with Person and Address are declared in their own configuration with a unique file URL and CloudKit container identifier to keep this data separate from the Trips graph. Finally, the Schema and configurations are combined to form the ModelContainer.
With the power of ModelConfiguration, it's easy to describe the persistence requirements of your application, no matter how complicated they may be. In addition to instantiating a container by hand, SwiftUI applications can use the new modelContainer modifiers to create the container they want to work with.
The modelContainer modifier can be added to any View or Scene in an application and it supports ModelContainers from simple to powerful and everything in between. In this section, I examined how to combine the Schema with persistence using ModelContainer. It grows with your application as you build ever more powerful features and object graphs. And I demonstrated how you can use ModelConfiguration to unlock powerful persistence capabilities. As we learned in "Meet SwiftData", the Model and ModelContext are two of the most frequently used concepts when writing user interfaces or operating on model objects. In this section, I'll take a deep dive into how the ModelContext tracks changes and persists edits through the ModelContainer.
When we use the modelContainer modifier in view or scene code, it prepares the application's environment in a specific way. The modifier binds the new modelContext key in the environment to the container's mainContext. The main context is a special MainActor-aligned model context intended for working with ModelObjects in scenes and views. By using the model context from the environment, view code has easy access to the context used by the Query here so that it can perform actions like delete here.
So model contexts are easy to use and access but what do they actually do? We can think of the ModelContext as a view over the data an application manages.
Data we want to work with is fetched into a model context as its used. In SampleTrips, when the upcoming trips view loads the data for the list, each trip object is fetched into the main context. If a trip is edited, that change is recorded by the model context as a snapshot. As other changes are made, like inserting a new Trip or deleting an existing one, the context tracks and maintains state about these changes until you call "context.save()". This means that even though the deleted trip is no longer visible in the list, it still exists in the ModelContext until that delete is persisted by calling save.
Once save is called, the context persists changes to the ModelContainer and clears its state.
If you're still referencing the objects in the context, like displaying them in a list, they will exist in the context until you're finished with them. At which point they will be freed and the context emptied out. The ModelContext works in coordination with the ModelContainer it is bound to. It tracks the objects you fetch in your views and then propagates any changes when save executes. ModelContext also supports features like rollback or reset for clearing its cached state when needed. This makes it the ideal place to support platform features like undo and autosave.
In SwiftUI applications, the modelContainer modifier has this isUndoEnabled argument, which binds the window's undo manager to the container's mainContext. That means that as changes are made in the main context, system gestures like three finger swipe and shake can be used to undo or redo changes with no additional code. ModelContext automatically registers undo and redo actions as changes are made to model objects. The modelContainer modifier uses the environment's undoManager which is usually provided by the system as part of the window or window group. and because of this, system gestures like three finger swipe and shake will automatically work in your applications. Another standard system feature supported by the ModelContext is Autosave. When autosave is enabled the model context will save in response to system events like an application entering the foreground or background. The main context will also periodically save as an application is used. Autosave is enabled by default in applications and can be disabled if desired using the modelContainer modifier's isAutosaveEnabled argument. Autosave is disabled for model contexts created by hand. In "Meet SwiftData", we learned a lot about how to work with ModelContext in an application and how well it pairs with SwiftUI. But user interfaces aren't the only places that applications need to work with model objects. In this section, I'll examine how SwiftData makes writing powerful, scalable code easier and safer than ever.
Tasks like working with data on a background queue, syncing with a remote server or other persistence mechanism, and batch processing all work with model objects, frequently in sets or graphs.
Many of these tasks will begin by fetching a set of objects to work with via the fetch method on a ModelContext. In this example, the FetchDescriptor for the Trip model tells Swift that the trips array will be a collection of Trip objects. There's no casting or complicated result tuples to worry about.
FetchDescriptor makes it easy to craft complicated queries using the new Predicate macro. For example, what are all the trips that involve staying at a specific hotel? Or what trips still have some activities that I need to make reservations for? In SwiftData, complicated queries that support subqueries and joins can all be written in pure swift. Predicate uses the Models you create and SwiftData uses the Schema generated from those models to translate these predicates into database queries. FetchDescriptor combines the power of the new Foundation Predicate macro with the Schema to bring compiler validated queries to persistence on Apple platforms for the first time. FetchDescriptor and related classes, like SortDescriptor, use generics to form the result type and tell the compiler about the properties of the model you can use. There are a number of tuning options you've come to know and love, like offset and limit, as well as parameters for faulting and prefetching.
All of this power combines in the new enumerate function on ModelContext. It's designed to help make the foiblesome pattern of batch traversal and enumeration implicitly efficient by encapsulating the platform best practices at a single call site. Enumerate works great with FetchDescriptors regardless of their complexity, from simple to powerful and everything in between. Enumerate automatically implements platform best practices for traversals like batching and mutation guards. These are customizable to meet the needs of your specific use case. For example, the batch size that enumerate uses defaults to 5,000 objects. But I could change it to 10,000 to reduce I/O operations during the traversal at the expense of memory growth. Heavier object graphs, like those that include images, movies, or other large data blobs, may choose to use a smaller batch size. Decreasing the batch size reduces memory growth but increases I/O during the enumeration. Enumerate also includes mutation guard by default. One of the most frequent causes of performance issues with large traversals is mutations that are trapped in the context during the enumeration. allowEscapingMutations tells enumerate that this is intentional, when not set, enumerate will throw if it discovers that the ModelContext performing the enumeration is dirty, preventing it from freeing objects that were already traversed.
In this session, we learned how to create powerful persistence configurations with Schema and ModelConfiguration. We also learned how easy it is to adopt standard system practices like undo and redo with the ModelContainer and ModelContext. And you can use SwiftData today to write safe, performant code in your project like never before with FetchDescriptor, predicate, and enumerate. I can't wait to see how you push the limits of what's possible with this new framework in the months and years ahead. Thanks for watching and happy coding.
-
-
1:45 - Trip model with cascading relationships
@Model final class Trip { var destination: String? var end_date: Date? var name: String? var start_date: Date? @Relationship(.cascade) var bucketListItem: [BucketListItem] = [BucketListItem]() @Relationship(.cascade) var livingAccommodation: LivingAccommodation? }
-
4:21 - Initializing a ModelContainer
// ModelContainer initialized with just Trip let container = try ModelContainer(for: Trip.self) // SwiftData infers related model classes as well let container = try ModelContainer( for: [ Trip.self, BucketListItem.self, LivingAccommodation.self ] )
-
5:41 - Using ModelConfiguration to customize ModelContainer
let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([Person.self, Address.self]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people)
-
6:49 - Creating ModelContainer in SwiftUI
@main struct TripsApp: App { let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([ Person.self, Address.self ]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people) var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
7:40 - Using the modelContainer modifier
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
7:50 - Referencing a ModelContext in SwiftUI views
struct ContentView: View { @Query var trips: [Trip] @Environment(\.modelContext) var modelContext var body: some View { NavigationStack (path: $path) { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) .swipeActions(edge: .trailing) { Button(role: .destructive) { modelContext.delete(trip) } label: { Label("Delete", systemImage: "trash") } } } .onDelete(perform: deleteTrips(at:)) } } } }
-
9:57 - Enabling undo on a ModelContainer
@main struct TripsApp: App { @Environment(\.undoManager) var undoManager var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isUndoEnabled: true) } }
-
11:05 - Enabling autosave on a ModelContainer
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isAutosaveEnabled: false) } }
-
11:54 - Fetching objects with FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) var trips = try context.fetch(FetchDescriptor<Trip>())
-
12:14 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) let hotelNames = ["First", "Second", "Third"] var predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { hotelNames.contains($0.placeName) }.count > 0 } var descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
12:27 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { $0.hasReservation == false }.count > 0 } descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
13:18 - Enumerating objects with FetchDescriptor
context.enumerate(FetchDescriptor<Trip>()) { trip in // Operate on trip }
-
13:36 - Enumerating with FetchDescriptor and SortDescriptor
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate(descriptor) { trip in // Remind me to make reservations for trip }
-
14:01 - Fine tuning enumerate with batchSize
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 10000 ) { trip in // Remind me to make reservations for trip }
-
14:28 - Fine tuning enumerate with batchSize and allowEscapingMutations
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 500, allowEscapingMutations: true ) { trip in // Remind me to make reservations for trip }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.