{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-274", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/274", "wordCount": 1933, "segments": [{"start": 7.0, "text": "Hi I'm Thomas, an Engineer on the SwiftData team."}, {"start": 11.0, "text": "In this session, I'm going to show you some of the new SwiftData features"}, {"start": 14.0, "text": "in Apple's 2027 releases."}, {"start": 18.0, "text": "If you're just starting on your SwiftData journey, make sure to watch the code-along"}, {"start": 22.0, "text": "\"Add persistence with SwiftData\" and learn how to adopt SwiftData in a new app."}, {"start": 28.0, "text": "Apple's 2027 releases bring exciting new features to SwiftData."}, {"start": 33.0, "text": "First, I'm going to show how to use Query"}, {"start": 36.0, "text": "to fetch your data in sections in SwiftUI views."}, {"start": 39.0, "text": "Next, I'm going to cover enhancements on how you can store"}, {"start": 42.0, "text": "custom types in your model."}, {"start": 45.0, "text": "And finally, I'll cover some great new APIs"}, {"start": 48.0, "text": "for observing model and history changes in your SwiftData store."}, {"start": 53.0, "text": "Let's have a look at sectioning."}, {"start": 55.0, "text": "Over the last years, we've been working on an App \"SampleTrips\" to let me track"}, {"start": 59.0, "text": "all the adventures I have planned."}, {"start": 62.0, "text": "SampleTrips uses SwiftData for Persistence and SwiftUI for the User Interface."}, {"start": 67.0, "text": "SwiftUI and SwiftData work great together."}, {"start": 71.0, "text": "Reading from SwiftData in SwiftUI is as easy as adding a Query to the view."}, {"start": 76.0, "text": "Here Query fetches all trips from SwiftData, sorted by the start date."}, {"start": 81.0, "text": "The results of this query can then be used in the view body to create a list of trips."}, {"start": 87.0, "text": "In SampleTrips I want to add the option to group trips by destination."}, {"start": 91.0, "text": "So when I have several trips planned to the same place,"}, {"start": 94.0, "text": "I can see them next to each other."}, {"start": 96.0, "text": "In Apple's 2027 releases, Query adds support for sectioning."}, {"start": 101.0, "text": "To create a sectioned query, I can pass a KeyPath"}, {"start": 104.0, "text": "that starts from the root of the Trip model"}, {"start": 107.0, "text": "and leads to a string to the new sectionBy parameter of Query."}, {"start": 111.0, "text": "Here I'm using a KeyPath to the Trips destination property to create a section"}, {"start": 116.0, "text": "for each destination."}, {"start": 119.0, "text": "The wrapped value of Query is still an array of Trip."}, {"start": 121.0, "text": "So right now the list looks like before."}, {"start": 124.0, "text": "To section the list, I'll wrap the current ForEach in a section"}, {"start": 128.0, "text": "and add a second ForEach."}, {"start": 130.0, "text": "To get to the sections, I access the query from the property wrapper,"}, {"start": 134.0, "text": "using the underscore-prefixed name \"_trips\"."}, {"start": 137.0, "text": "The query has a sections property that returns a list of sections."}, {"start": 141.0, "text": "Then I can use SwiftUI's ForEach to iterate through all sections."}, {"start": 146.0, "text": "Each section has an ID property."}, {"start": 149.0, "text": "The ID is the value of my model from the KeyPath"}, {"start": 152.0, "text": "I passed to the SectionBy parameter of Query."}, {"start": 155.0, "text": "So here in my TripListView the ID"}, {"start": 157.0, "text": "will be the destination of all trips in the section."}, {"start": 163.0, "text": "I use ID as the Section's header label."}, {"start": 166.0, "text": "The section itself is a collection of trips - so I change the inner ForEach"}, {"start": 170.0, "text": "to iterate though them and create a TripListItem for each trip in the section."}, {"start": 176.0, "text": "Now, that I can see all my trips grouped by their destination,"}, {"start": 179.0, "text": "let's move to the next section."}, {"start": 182.0, "text": "And have a closer look at some enhancements on storing"}, {"start": 184.0, "text": "custom types in your model."}, {"start": 187.0, "text": "At the center of the SampleTrips app is the Trip model."}, {"start": 191.0, "text": "It stores information like name, destination, and the start and end dates."}, {"start": 196.0, "text": "Right now the destination is stored as a String in the Trip Model."}, {"start": 200.0, "text": "I want add the ability to find locations from MapKit."}, {"start": 204.0, "text": "I've already built a picker in the App that lets me search for destinations"}, {"start": 208.0, "text": "using MapKits search API."}, {"start": 211.0, "text": "For each location, the MapKit API provides a lot of metadata, such as the name,"}, {"start": 216.0, "text": "geographic coordinates and a MKMapItem.Identifier."}, {"start": 220.0, "text": "This MKMapItem.Identifier uniquely identifies the location in MapKit"}, {"start": 225.0, "text": "and can be used to look up more metadata about the location"}, {"start": 228.0, "text": "and open the location in the Maps app."}, {"start": 230.0, "text": "I want to store the coordinates"}, {"start": 232.0, "text": "and the MKMapItem.Identifier in the trip model."}, {"start": 236.0, "text": "So I'm adding them to the Trip class."}, {"start": 239.0, "text": "But now, when I launch the app, SwiftData raises a Fatal Error -"}, {"start": 243.0, "text": "\"Class property within Persisted Struct/Enum is not supported\" -"}, {"start": 247.0, "text": "pointing at MKMapItem.Identifier as the cause."}, {"start": 251.0, "text": "When SwiftData loads, it automatically generates a schema for the models."}, {"start": 255.0, "text": "The schema defines a mapping between the Model classes and the entities"}, {"start": 259.0, "text": "and properties a DataStore can persist."}, {"start": 262.0, "text": "For most types this works automatically."}, {"start": 264.0, "text": "But classes that are not annotated with the Model macro can't be automatically"}, {"start": 269.0, "text": "inspected and SwiftData fails to generate a schema."}, {"start": 272.0, "text": "Since MKMapItem.Identifier comes from MapKit,"}, {"start": 275.0, "text": "I don't have access to its implementation."}, {"start": 278.0, "text": "I can't modify it and mark it as @Model, and SwiftData can't inspect it"}, {"start": 282.0, "text": "because it's a class."}, {"start": 284.0, "text": "But it does conform to Codable."}, {"start": 286.0, "text": "Codable types know how to serialize themselves into data"}, {"start": 290.0, "text": "and instantiate themselves back from it."}, {"start": 292.0, "text": "In Apple's 2027 releases, you can mark a model attribute as .codable"}, {"start": 297.0, "text": "to tell SwiftData to delegate serialization to the type"}, {"start": 301.0, "text": "and store the serialized representation."}, {"start": 304.0, "text": "Let's mark the mapItemIdentifier attribute as codable."}, {"start": 308.0, "text": "Now, SampleTrips launches without any error."}, {"start": 311.0, "text": "Great."}, {"start": 312.0, "text": "The codable attribute tells SwiftData to persist the encoded representation"}, {"start": 317.0, "text": "instead of inferring a schema for the type."}, {"start": 320.0, "text": "Like transformable attributes, codable comes with a few things to keep in mind."}, {"start": 325.0, "text": "The contents of codable attributes are opaque to SwiftData."}, {"start": 329.0, "text": "This means they can't be used in Predicates to filter results"}, {"start": 332.0, "text": "or for sorting - using Sort Descriptors."}, {"start": 335.0, "text": "Also, if the shape of the codable type changes,"}, {"start": 338.0, "text": "like adding or removing properties,"}, {"start": 340.0, "text": "they will not trigger a migration."}, {"start": 342.0, "text": "The Codable implementation of the type must be able to encode and decode"}, {"start": 347.0, "text": "in a forward- and backward-compatible way."}, {"start": 351.0, "text": "Using Codable attributes can be thought of as an \"escape hatch\" to persist types"}, {"start": 355.0, "text": "that SwiftData does not support natively."}, {"start": 359.0, "text": "You should avoid using codable for Types that you define."}, {"start": 362.0, "text": "Modeling them as SwiftData models or supported values types, allows you to"}, {"start": 365.0, "text": "harness the full power of SwiftData, like Sorting, Filtering, and Indexing."}, {"start": 370.0, "text": "But for types that you don't own,"}, {"start": 372.0, "text": "codable lets you store them alongside your other attributes."}, {"start": 376.0, "text": "Now I can pick locations from MapKit in SampleTrips and persist"}, {"start": 380.0, "text": "the destination's MKMapItem.Identifier"}, {"start": 382.0, "text": "alongside the other properties in the trip model."}, {"start": 386.0, "text": "Now, let's look at how you can monitor your store"}, {"start": 389.0, "text": "and be notified when data changes."}, {"start": 392.0, "text": "Earlier we used the query macro in the TripListView to make trips"}, {"start": 396.0, "text": "from the SwiftData store available to SwiftUI."}, {"start": 401.0, "text": "@Query is a powerful tool."}, {"start": 402.0, "text": "When the view appears, it fetches models"}, {"start": 404.0, "text": "from the SwiftData store"}, {"start": 405.0, "text": "so they can be used in the view body."}, {"start": 408.0, "text": "Then, it continuously monitors the store for changes that would affect its results."}, {"start": 414.0, "text": "For example, when a trip is removed from the store,"}, {"start": 417.0, "text": "Query will notice the change"}, {"start": 419.0, "text": "and the view re-renders to reflect the new results of the query."}, {"start": 423.0, "text": "Query is great and should be your first choice in a SwiftUI view."}, {"start": 427.0, "text": "But what about parts of your app that are not SwiftUI?"}, {"start": 430.0, "text": "Maybe you have a state object that derives values from your SwiftData store"}, {"start": 434.0, "text": "and needs to recompute when the data changes."}, {"start": 436.0, "text": "Or your app doesn't use SwiftUI at all like a game written in SceneKit?"}, {"start": 441.0, "text": "In Apple's 2027 releases, we're introducing ResultsObserver."}, {"start": 446.0, "text": "Like Query in SwiftUI views, ResultsObserver fetches data"}, {"start": 450.0, "text": "from your SwiftData store and then observes your store for changes."}, {"start": 453.0, "text": "But it works anywhere in your app - independent of SwiftUI views -"}, {"start": 457.0, "text": "using Swift Observation."}, {"start": 460.0, "text": "It supports the same query primitives that you already know -"}, {"start": 463.0, "text": "filtering, sorting, and the sectioning we covered earlier."}, {"start": 468.0, "text": "Here's the data flow diagram I shared earlier,"}, {"start": 471.0, "text": "but with the Query replaced with ResultsObserver"}, {"start": 474.0, "text": "and the View replaced with any code you like."}, {"start": 477.0, "text": "The ResultsObserver fetches data and observes your store for changes."}, {"start": 481.0, "text": "Your code can use Swift Observation to react to these changes."}, {"start": 486.0, "text": "Earlier I've added location information in the SampleTrips app."}, {"start": 490.0, "text": "I want to add a map to see all the places I have trips planned to."}, {"start": 493.0, "text": "I am using SwiftUIs MapKit integration."}, {"start": 496.0, "text": "But I want to customize what area of the map is shown."}, {"start": 500.0, "text": "To do this, I've added a new MapCameraController,"}, {"start": 503.0, "text": "which calculates a fitting MapCameraBounds for the map."}, {"start": 507.0, "text": "It uses ResultsObserver to know when to recalculate the MapCameraBounds."}, {"start": 513.0, "text": "To do this, I create a ResultsObserver for trips."}, {"start": 516.0, "text": "For the map, I always want to show all trips,"}, {"start": 519.0, "text": "so I'm not going to pass in a predicate to filter or a sectioning key path."}, {"start": 524.0, "text": "Then, I'm using withContinuousObservation with the didSet option"}, {"start": 529.0, "text": "to get a callback every time a trip changes."}, {"start": 532.0, "text": "And when the results are changing, I'm re-calculating the bounds of the map."}, {"start": 537.0, "text": "withContinuousObservation returns an ObservationTracking token."}, {"start": 541.0, "text": "This token defines the lifetime of the observation."}, {"start": 543.0, "text": "I'll store this token on my class to receive updates"}, {"start": 546.0, "text": "for the entire lifetime of my MapCameraController."}, {"start": 551.0, "text": "Here it is in action."}, {"start": 552.0, "text": "When I launch SampleTrips, MapCameraController picked"}, {"start": 555.0, "text": "the map camera bounds that fit all my trips into the view."}, {"start": 559.0, "text": "Unfortunately, I won't be able to visit Toronto later this month,"}, {"start": 562.0, "text": "so I'm going to delete this trip."}, {"start": 565.0, "text": "The ResultsObserver will notice the change and the MapCameraController"}, {"start": 569.0, "text": "will recalculate the camera bounds of the map."}, {"start": 573.0, "text": "ResultsObserver is a powerful tool if your code needs to react to changes"}, {"start": 577.0, "text": "in your data store - anywhere in your app."}, {"start": 579.0, "text": "But we didn't stop there."}, {"start": 581.0, "text": "In Apple's 2027 releases, we're also supporting observing history."}, {"start": 586.0, "text": "First, a quick history lesson."}, {"start": 588.0, "text": "When data in your store changes, SwiftData keeps a record of everything that changed."}, {"start": 593.0, "text": "You can use this history to build features"}, {"start": 595.0, "text": "like syncing with external servers,"}, {"start": 597.0, "text": "or reacting to changes made outside of your app,"}, {"start": 599.0, "text": "like in an app extension."}, {"start": 601.0, "text": "Every time your data store is saved, SwiftData records a history transaction."}, {"start": 605.0, "text": "The history transaction contains information about what changed,"}, {"start": 609.0, "text": "and where the change was coming from -"}, {"start": 611.0, "text": "and a token that uniquely identifies the transaction in the history."}, {"start": 615.0, "text": "This token can be used with the ModelContext.fetchHistory API"}, {"start": 619.0, "text": "to fetch newer transactions."}, {"start": 621.0, "text": "To learn more about persistent history, watch"}, {"start": 624.0, "text": "\"Track Model Changes with SwiftData History\" from WWDC 2024."}, {"start": 630.0, "text": "New in Apple's 2027 releases is HistoryObserver."}, {"start": 634.0, "text": "HistoryObserver makes it easy to react to any changes in your store."}, {"start": 638.0, "text": "Similar to ResultsObserver observing fetch results, HistoryObserver can observe"}, {"start": 643.0, "text": "the persistent history and let your code react when new transactions are added."}, {"start": 649.0, "text": "And if you only need to know about certain kinds of changes,"}, {"start": 652.0, "text": "HistoryObserver lets you filter by model type and transaction author."}, {"start": 656.0, "text": "Observing history is useful"}, {"start": 658.0, "text": "when your app needs to keep parts of the data store in sync"}, {"start": 661.0, "text": "with other systems, like an external server."}, {"start": 665.0, "text": "HistoryObserver has a single observable property - eventCounter."}, {"start": 669.0, "text": "When new transactions are available in the persistent history,"}, {"start": 673.0, "text": "the eventCounter increments."}, {"start": 675.0, "text": "Your code can observe the eventCounter and when it increments,"}, {"start": 678.0, "text": "use ModelContext.fetchHistory API to fetch the latest changes."}, {"start": 684.0, "text": "Here's an example of how you could use HistoryObserver to synchronize changes,"}, {"start": 689.0, "text": "made in the app with an external server."}, {"start": 692.0, "text": "First, I set up a HistoryObserver for my modelContainer."}, {"start": 697.0, "text": "In this case, I'm only interested in changes made by the app"}, {"start": 701.0, "text": "so I'm passing \"App\" as authors."}, {"start": 703.0, "text": "So that I'm not replaying changes that came from the server back to the server."}, {"start": 708.0, "text": "Next, I'm using withContinuousObservation."}, {"start": 712.0, "text": "Like the MapCameraController,"}, {"start": 713.0, "text": "I store the observation tracking token on the class"}, {"start": 717.0, "text": "so that tracking remains active for the lifetime of my class."}, {"start": 721.0, "text": "Within the observation closure I need to access eventCounter"}, {"start": 724.0, "text": "so that Swift Observation knows what to track."}, {"start": 728.0, "text": "And finally, I call my processChanges function."}, {"start": 731.0, "text": "In processChanges I can use the ModelContext.fetchHistory API"}, {"start": 735.0, "text": "to fetch history and upload changes to my server."}, {"start": 739.0, "text": "I hope you are as excited as I am"}, {"start": 741.0, "text": "about the new features in SwiftData in our 2027 releases."}, {"start": 746.0, "text": "Tailor SwiftData fetches with sections."}, {"start": 748.0, "text": "Use codable attributes when storing types from other frameworks."}, {"start": 752.0, "text": "Use ResultsObserver to react to changes of query results outside of SwiftUI views."}, {"start": 757.0, "text": "And finally,"}, {"start": 758.0, "text": "use HistoryObserver to react to changes in SwiftData's persistent history."}, {"start": 764.0, "text": "Thanks for watching."}]}