{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-275", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/275", "wordCount": 3133, "segments": [{"start": 7.0, "text": "Hello, I'm Matthew Turk, an engineer on the SwiftData team."}, {"start": 11.0, "text": "Today, I'd like to show you how to take an existing SwiftUI app's dynamic data"}, {"start": 16.0, "text": "and connect it to a modern persistence layer that works across all of Apple's platforms"}, {"start": 21.0, "text": "using the power of SwiftData."}, {"start": 23.0, "text": "We'll start with the source code of a list-based app called Wishlist."}, {"start": 28.0, "text": "Wishlist helps me organize travel plans on my phone by recording my ideas"}, {"start": 32.0, "text": "and grouping trips into seasonal collections."}, {"start": 35.0, "text": "Feel free to download the sample app from developer.apple.com to follow along!"}, {"start": 42.0, "text": "In this video, we'll step through the project files to identify data types"}, {"start": 46.0, "text": "and variables to use for our SwiftData models and schemas,"}, {"start": 50.0, "text": "how those models should relate to one another in a database"}, {"start": 53.0, "text": "and how to update our SwiftUI views"}, {"start": 56.0, "text": "to present the new models with an eye toward performance,"}, {"start": 59.0, "text": "interoperability, and extensibility for more involved use cases."}, {"start": 64.0, "text": "Let's preview what that data flow is like in action."}, {"start": 69.0, "text": "Here's the Wishlist tab, with recent trips at the top,"}, {"start": 72.0, "text": "and several themed lists of more trips scrolling down."}, {"start": 77.0, "text": "In the Goals tab, I can see badges for various goals"}, {"start": 80.0, "text": "that I'd like to complete on those trips."}, {"start": 83.0, "text": "And in the Search tab, I can filter through my trips and activities."}, {"start": 87.0, "text": "Let's search for coastal trails."}, {"start": 89.0, "text": "Top result."}, {"start": 93.0, "text": "This trip has five remaining activities."}, {"start": 95.0, "text": "I already drove to Point Reyes once before and saw 11 elk there,"}, {"start": 99.0, "text": "so I'll check this one off."}, {"start": 104.0, "text": "And back on the Wishlist tab, I'll tap the plus button to add a new trip."}, {"start": 110.0, "text": "I'd like to see another aurora sometime."}, {"start": 112.0, "text": "I'll call the trip \"Northern Lights,\" and choose a photo to go with it."}, {"start": 121.0, "text": "Press Done, and that's my new trip."}, {"start": 125.0, "text": "The data flow you just saw is possible because views in Wishlist"}, {"start": 128.0, "text": "pull in a DataSource variable through the SwiftUI environment."}, {"start": 132.0, "text": "The DataSource class manages and provides all of the preinstalled trip data."}, {"start": 137.0, "text": "All trips, goals, and search results are filtered and sorted in memory, on demand."}, {"start": 142.0, "text": "For a small example, this works fine."}, {"start": 145.0, "text": "But in practice, optimizations will be necessary"}, {"start": 148.0, "text": "to keep the front-facing parts of the app lean."}, {"start": 151.0, "text": "Additionally, Wishlist is relying on RAM"}, {"start": 154.0, "text": "while processing this data and for storing it,"}, {"start": 157.0, "text": "which is not a stable place where you can save new trips or activities for later."}, {"start": 163.0, "text": "If I close the app and rerun it,"}, {"start": 166.0, "text": "everything about my new trip is gone,"}, {"start": 169.0, "text": "reset to the preinstalled content."}, {"start": 174.0, "text": "But it doesn't have to be this way."}, {"start": 176.0, "text": "This is exactly the kind of problem that SwiftData solves."}, {"start": 180.0, "text": "So far, we've identified the relevant state,"}, {"start": 183.0, "text": "namely trip collections, goal statuses, and search results."}, {"start": 188.0, "text": "And SwiftData can connect that state"}, {"start": 190.0, "text": "to a persistent storage medium through a model context,"}, {"start": 193.0, "text": "and that's where we'll be by the end of this video."}, {"start": 197.0, "text": "Our next move is to find where these data structures live in the code"}, {"start": 200.0, "text": "and then refactor them as models with SwiftData schemas."}, {"start": 204.0, "text": "That will set us up to replace the in-memory DataSource"}, {"start": 207.0, "text": "with a persistent ModelContext,"}, {"start": 209.0, "text": "which will enable us to write efficient database queries to drive the views."}, {"start": 214.0, "text": "Let's take a closer look at one such model, the Activity type."}, {"start": 219.0, "text": "To persist Activities, the first step is to import SwiftData"}, {"start": 224.0, "text": "and replace this Observable macro with the Model macro."}, {"start": 229.0, "text": "SwiftData automatically generates observable conformance for us."}, {"start": 233.0, "text": "Nice!"}, {"start": 234.0, "text": "On a side note, these didSet observers on name and isComplete"}, {"start": 240.0, "text": "set the activity's dateEdited value whenever either property changes."}, {"start": 247.0, "text": "Suppose I have an activity for going paragliding."}, {"start": 250.0, "text": "I've yet to complete it and last edited it on April 1 at 9:41 in the morning."}, {"start": 257.0, "text": "Here's a timeline of events."}, {"start": 258.0, "text": "Suppose that I then decide to change the activity from paragliding to swimming."}, {"start": 264.0, "text": "The property observer for name fires and updates the dateEdited."}, {"start": 268.0, "text": "Later, I take a trip to the beach and check off the activity."}, {"start": 273.0, "text": "The property observer for isComplete fires and updates the dateEdited."}, {"start": 278.0, "text": "Having an automatically updating dateEdited property is a great way"}, {"start": 282.0, "text": "to sort or filter in a list."}, {"start": 285.0, "text": "However, property observers and computed properties are not always compatible,"}, {"start": 289.0, "text": "so I'll use a different technique to keep dateEdited up to date."}, {"start": 294.0, "text": "But before I do that, I'll get the current project building again."}, {"start": 298.0, "text": "I'll come back to this diagram."}, {"start": 300.0, "text": "For now, just remove these didSet blocks"}, {"start": 311.0, "text": "and leave dateEdited as is."}, {"start": 314.0, "text": "Okay, the Trip class is next."}, {"start": 316.0, "text": "Same deal, import SwiftData,"}, {"start": 321.0, "text": "and swap the macros."}, {"start": 328.0, "text": "Now let's inspect these build failures to get an idea of what's missing."}, {"start": 333.0, "text": "This one says that creationDate has to be mutable."}, {"start": 336.0, "text": "So we'll adjust the declaration to use var instead of let."}, {"start": 343.0, "text": "Now SwiftData can populate this property at runtime"}, {"start": 345.0, "text": "with a value loaded from the database."}, {"start": 348.0, "text": "The next error says the TripCollection property,"}, {"start": 351.0, "text": "and all model properties, must be Codable."}, {"start": 354.0, "text": "This requirement exists"}, {"start": 355.0, "text": "so that SwiftData can serialize properties into database columns."}, {"start": 360.0, "text": "The trip collection stores the seasonal theme of the trip."}, {"start": 363.0, "text": "It should be included in the schema and persisted."}, {"start": 366.0, "text": "I'll command-click to jump to its declaration."}, {"start": 370.0, "text": "And I'll add that Codable conformance explicitly."}, {"start": 373.0, "text": "Several build failures remain."}, {"start": 375.0, "text": "We'll fix these as we continue to build out the schema."}, {"start": 378.0, "text": "Next, let's look at how goal tracking works in Wishlist."}, {"start": 382.0, "text": "With this Goal type, the change is not as straightforward as converting"}, {"start": 386.0, "text": "an Observable to a Model."}, {"start": 389.0, "text": "Goal is declared as an enumeration."}, {"start": 391.0, "text": "Each goal has properties like its name, its kind—like whether it tracks activities"}, {"start": 396.0, "text": "completed or trips completed—"}, {"start": 398.0, "text": "and the target number of trips or activities to achieve the goal."}, {"start": 403.0, "text": "There are exactly 18 Goals, all defined ahead of time,"}, {"start": 406.0, "text": "because an enumeration defines a closed set of values."}, {"start": 410.0, "text": "That's fine for the original interface demo,"}, {"start": 413.0, "text": "but what we need for a persistent model is a class."}, {"start": 417.0, "text": "A class can store properties and can be instantiated any number of times."}, {"start": 422.0, "text": "To get the interface right, we'll need to rethink the design of Goal,"}, {"start": 426.0, "text": "so that it harmonizes with the logic of how Goals are processed and presented."}, {"start": 432.0, "text": "At a high level, each of these goals corresponds to a badge"}, {"start": 436.0, "text": "that will be displayed in Wishlist depending on whether certain criteria are met."}, {"start": 441.0, "text": "To faithfully persist the statuses of these goals,"}, {"start": 444.0, "text": "we need some way to capture and store a minimal representation"}, {"start": 448.0, "text": "of whether a particular goal's criteria have been met."}, {"start": 452.0, "text": "In SwiftData, a class is the substrate we choose to express"}, {"start": 455.0, "text": "that minimal representation."}, {"start": 458.0, "text": "I'll convert Goal into a class starting with the same three properties:"}, {"start": 463.0, "text": "name, kind, and target count."}, {"start": 467.0, "text": "In the original Wishlist app, the number of items completed was stored separately,"}, {"start": 471.0, "text": "since enumerations don't have stored properties."}, {"start": 475.0, "text": "Now that we have a persistent class,"}, {"start": 477.0, "text": "let's also store the progress value in the goal itself,"}, {"start": 480.0, "text": "called completedCount,"}, {"start": 483.0, "text": "and a Boolean property called isComplete."}, {"start": 486.0, "text": "We'll store a value of true when completedCount"}, {"start": 489.0, "text": "is greater than or equal to target."}, {"start": 491.0, "text": "Because it's a stored property, it'll come in handy for separating completed goals"}, {"start": 496.0, "text": "from upcoming goals when we start querying in the view layer."}, {"start": 501.0, "text": "Next we have to deal with the Kind property."}, {"start": 504.0, "text": "Wishlist uses it to display different details based on whether the goal"}, {"start": 507.0, "text": "has to do with completing trips or completing activities."}, {"start": 511.0, "text": "We can break off these finer differences into new subclasses"}, {"start": 515.0, "text": "using SwiftData's support for model inheritance."}, {"start": 519.0, "text": "Inheritance is a software design pattern that tends to pay off when you have"}, {"start": 523.0, "text": "a well-defined hierarchy of classes, where each subclass represents the same idea"}, {"start": 528.0, "text": "as the superclass,"}, {"start": 530.0, "text": "but with more specific manifestations of a common set of properties."}, {"start": 534.0, "text": "For example, a spiral galaxy and a lenticular galaxy"}, {"start": 538.0, "text": "are subclasses of galaxy,"}, {"start": 540.0, "text": "as they inherit several features that you'll find in any kind of galaxy,"}, {"start": 544.0, "text": "like stars and dust,"}, {"start": 546.0, "text": "while also having categorical differences in visible shape."}, {"start": 550.0, "text": "Similarly, you could have a trip goal and an activity goal,"}, {"start": 553.0, "text": "which inherit some common properties from a superclass of goal."}, {"start": 558.0, "text": "I'll model the different kinds of goals with inheritance"}, {"start": 561.0, "text": "by removing the Kind property from Goal."}, {"start": 564.0, "text": "And introducing subclasses for TripGoals and ActivityGoals."}, {"start": 569.0, "text": "To learn more about when it makes sense to use model inheritance, check out the"}, {"start": 573.0, "text": "\"SwiftData: Dive into inheritance and schema migration\" session from WWDC 2025."}, {"start": 581.0, "text": "We now have everything we need to start defining the relationships"}, {"start": 584.0, "text": "between all of these models."}, {"start": 586.0, "text": "In Wishlist, each trip is associated with a set of activities."}, {"start": 591.0, "text": "This is a to-many relationship."}, {"start": 593.0, "text": "Each persistent Trip model has potentially many persistent Activity models."}, {"start": 599.0, "text": "Several parts of Wishlist have been approximating to-many relationships"}, {"start": 603.0, "text": "by using dictionaries and functions that loop over arrays."}, {"start": 608.0, "text": "For example, there's one that maps from Trips to Activities based on activity IDs."}, {"start": 614.0, "text": "In a moment, I'll convert these dictionaries to proper"}, {"start": 617.0, "text": "to-many relationships between types,"}, {"start": 619.0, "text": "where each Trip can have zero or more Activities."}, {"start": 623.0, "text": "Declaring an array is the idiomatic way to tell SwiftData that one kind of model"}, {"start": 628.0, "text": "may reference another kind of model from the model context as needed."}, {"start": 633.0, "text": "In Trip, I'll declare an array of activities."}, {"start": 636.0, "text": "Here I'm also adding the relationship macro to explicitly mark the activities"}, {"start": 641.0, "text": "array of Trip as a relationship so that deleting a trip from the database"}, {"start": 646.0, "text": "also clears out the activity models that were in its itinerary."}, {"start": 650.0, "text": "Lastly, this photoURL property needs to be adjusted."}, {"start": 655.0, "text": "It's just a file path right now, which will lose all meaning"}, {"start": 658.0, "text": "if the file is renamed or moved to another directory."}, {"start": 662.0, "text": "And, the full-resolution image should only be loaded"}, {"start": 665.0, "text": "when a view needs to show it in full, like in TripDetailView."}, {"start": 670.0, "text": "When scrolling through a carousel of trips, we'll just show the thumbnail."}, {"start": 674.0, "text": "So add a new property here called thumbnailData."}, {"start": 678.0, "text": "This will cache a low-resolution version of a selected photo"}, {"start": 682.0, "text": "and inline its raw bytes in the database."}, {"start": 685.0, "text": "Then separately, instead of the URL, we'll store the full-resolution image"}, {"start": 690.0, "text": "using a persistent external file reference."}, {"start": 693.0, "text": "In SwiftData, this is done by creating a new model just for the image."}, {"start": 698.0, "text": "I already added that as the TripImage type."}, {"start": 701.0, "text": "We won't cover those implementation details here,"}, {"start": 704.0, "text": "but I encourage you to read more in the sample code later."}, {"start": 707.0, "text": "Since photoURL is no longer a URL, let's rename it to photo by right-clicking on"}, {"start": 713.0, "text": "the declaration and selecting Rename in this refactor menu."}, {"start": 718.0, "text": "I'm using multi-cursor editing, so multiple files will be updated at once."}, {"start": 724.0, "text": "And while I'm at it, I can click this comment to update it with the new variable name."}, {"start": 729.0, "text": "Hit Return."}, {"start": 730.0, "text": "Then refactor the labels of the initializer"}, {"start": 743.0, "text": "and adjust the body to set activities directly."}, {"start": 750.0, "text": "By setting up this relationship, we've replicated the ability"}, {"start": 753.0, "text": "from before for an activity-driven view to reference and display the name, season,"}, {"start": 759.0, "text": "and other details of its parent trip."}, {"start": 761.0, "text": "And now that we've really started integrating these SwiftData features,"}, {"start": 765.0, "text": "the project has a few surplus files."}, {"start": 768.0, "text": "There's no need for TripEditModel anymore since SwiftUI views can bind directly to"}, {"start": 773.0, "text": "SwiftData models and propagate edits in real time."}, {"start": 777.0, "text": "All the responsibilities of DataSource are handled automatically by ModelContext,"}, {"start": 782.0, "text": "queries, and relationships."}, {"start": 784.0, "text": "Delete."}, {"start": 786.0, "text": "It's worth reflecting on that last part."}, {"start": 789.0, "text": "We just removed hundreds of lines of code from the project."}, {"start": 792.0, "text": "A good chunk of state management, storage logic, filtering, sorting,"}, {"start": 796.0, "text": "relationship traversal, and search will just work."}, {"start": 801.0, "text": "One last touch on the WindowGroup, and the model layer can be done."}, {"start": 805.0, "text": "This modelContainer scene modifier here tells SwiftUI"}, {"start": 809.0, "text": "to use our new schema with the query macro."}, {"start": 812.0, "text": "Next, we'll update the view layer."}, {"start": 815.0, "text": "With the schemas and model container in place,"}, {"start": 818.0, "text": "automatic saving is enabled by default."}, {"start": 821.0, "text": "Before seeing that in action, we're going to integrate these persistent models,"}, {"start": 826.0, "text": "starting with efficient, targeted queries for each subview that presents models."}, {"start": 831.0, "text": "Then, we'll use SwiftUI view modifiers to capture and surface possible errors"}, {"start": 836.0, "text": "that could occur at runtime, like low disk capacity or unsupported predicates."}, {"start": 842.0, "text": "Lastly, we'll add back any missing property observers"}, {"start": 845.0, "text": "so that UI events are propagating all of the right data"}, {"start": 849.0, "text": "and side effects that we expect."}, {"start": 851.0, "text": "There are two key points to keep in mind when adding filtering to your SwiftData app."}, {"start": 856.0, "text": "Inside your queries, the FetchDescriptor is how you plan which models you want"}, {"start": 860.0, "text": "to load and show through your model context or query."}, {"start": 864.0, "text": "And secondly, your models will be saved to a storage medium that exists outside of"}, {"start": 869.0, "text": "the address space of your app,"}, {"start": 871.0, "text": "such as a database in your local filesystem or a remote server."}, {"start": 875.0, "text": "While this kind of storage is the bedrock of a persistence layer,"}, {"start": 879.0, "text": "it can be orders of magnitude slower than reading from memory,"}, {"start": 883.0, "text": "so when designing a persistence layer in your app,"}, {"start": 885.0, "text": "it's critical to think about which data need to be where and when"}, {"start": 889.0, "text": "for the best experience."}, {"start": 891.0, "text": "I'll explain."}, {"start": 893.0, "text": "Before, data about trips, activities, and goals"}, {"start": 896.0, "text": "would be in global variables like allGoals."}, {"start": 900.0, "text": "They were loaded as part of the compiled binary for the app."}, {"start": 903.0, "text": "Accessing data this way is fast, but if I hardcode lots of elements"}, {"start": 908.0, "text": "into allGoals, for example,"}, {"start": 910.0, "text": "the app will have a noticeably elevated memory footprint"}, {"start": 913.0, "text": "for its entire lifetime."}, {"start": 915.0, "text": "And if I add a new goal, everything about it disappears"}, {"start": 919.0, "text": "when I close the app"}, {"start": 920.0, "text": "and relaunch it as you've seen."}, {"start": 923.0, "text": "With SwiftData, we can insert goals or update existing goals and save them"}, {"start": 928.0, "text": "with our model context."}, {"start": 930.0, "text": "Once they're saved, there are a few ways we can pull them back into the app."}, {"start": 935.0, "text": "Here's one way."}, {"start": 936.0, "text": "This code fetches all of the Goals and then discards the irrelevant ones."}, {"start": 941.0, "text": "So it uses less ongoing memory, but it incurs more I/O."}, {"start": 946.0, "text": "This approach is a little like asking a librarian to go grab every book"}, {"start": 950.0, "text": "from every shelf at a library,"}, {"start": 952.0, "text": "so that you can personally identify the ones by your favorite author."}, {"start": 956.0, "text": "You could have asked that librarian for just the books by that author"}, {"start": 959.0, "text": "on their trip through the shelves."}, {"start": 962.0, "text": "When you fetch using a predicate, it's like asking that librarian upfront"}, {"start": 966.0, "text": "for what you want."}, {"start": 968.0, "text": "Here, I get only the goals that I asked for."}, {"start": 972.0, "text": "In GoalsView, we're going to use the query macro with a predicate."}, {"start": 976.0, "text": "This is equivalent to calling fetch on a model context."}, {"start": 980.0, "text": "The advantage is that the SwiftUI view will update automatically"}, {"start": 984.0, "text": "when the query result changes."}, {"start": 986.0, "text": "Let's do that now."}, {"start": 987.0, "text": "Import SwiftData"}, {"start": 991.0, "text": "and replace this dataSource environment property"}, {"start": 994.0, "text": "with a query for fetching achieved goals, sorted by when they were achieved."}, {"start": 999.0, "text": "And this second query fetches the remaining relevant goals."}, {"start": 1003.0, "text": "Same idea in RecentTripsPageView."}, {"start": 1009.0, "text": "Import SwiftData"}, {"start": 1014.0, "text": "and replace dataSource with a query."}, {"start": 1018.0, "text": "We'll ask for trips in reverse chronological order"}, {"start": 1021.0, "text": "and set a fetch limit of 5."}, {"start": 1023.0, "text": "That gets us the five most recent trips, which will go right into this ForEach."}, {"start": 1029.0, "text": "In TripCollectionView, we want to get all the trips and segment them by season."}, {"start": 1034.0, "text": "Each season is a trip collection,"}, {"start": 1036.0, "text": "and there's going to be one instance of this view for each tripCollection."}, {"start": 1040.0, "text": "An individual TripCollectionView doesn't know which season it's going to display"}, {"start": 1045.0, "text": "until it's initialized,"}, {"start": 1047.0, "text": "so we'll declare the query"}, {"start": 1051.0, "text": "and then dynamically construct it in the initializer."}, {"start": 1055.0, "text": "Here's the explicit query for all trips matching the desired Collection."}, {"start": 1060.0, "text": "Notice that the query receives a predicate, and inside of the predicate,"}, {"start": 1064.0, "text": "the tripCollection parameter is captured directly from the initializer"}, {"start": 1069.0, "text": "before heading to the database."}, {"start": 1071.0, "text": "And the results of the query will go into this ForEach."}, {"start": 1079.0, "text": "Next we have SearchResultsListView in the third tab in the app."}, {"start": 1083.0, "text": "Its super view, which is shown in the preview on the right,"}, {"start": 1087.0, "text": "owns the search field and text, and passes the value to this view's initializer."}, {"start": 1092.0, "text": "So once again, replace dataSource with the query declarations,"}, {"start": 1097.0, "text": "and then the parameters from the initializer"}, {"start": 1099.0, "text": "will guide how the predicates are constructed for these queries."}, {"start": 1104.0, "text": "If the search text is empty, fall back to fetching the three most recent trips."}, {"start": 1109.0, "text": "Otherwise, fetch all trips whose name matches the search text, sorted lexicographically."}, {"start": 1115.0, "text": "And then we'll do the same for activity search."}, {"start": 1118.0, "text": "Check if the text matches the name of any activities belonging to a trip,"}, {"start": 1122.0, "text": "and again set the property wrapper like so."}, {"start": 1129.0, "text": "Lastly, use the queried values in List."}, {"start": 1136.0, "text": "This list also has an overlay view modifier,"}, {"start": 1139.0, "text": "which displays a ContentUnavailableView on the top if no search results are found."}, {"start": 1145.0, "text": "Let's replace the original dataSource condition with direct checks on"}, {"start": 1149.0, "text": "trips.isEmpty and activities.isEmpty."}, {"start": 1155.0, "text": "That's enough to get the app running again."}, {"start": 1157.0, "text": "I'll add a trip in Wishlist and rerun it."}, {"start": 1168.0, "text": "This time, my Trip is still there."}, {"start": 1172.0, "text": "\"Northern Lights.\""}, {"start": 1174.0, "text": "The transition to SwiftData is almost complete."}, {"start": 1179.0, "text": "We have just a few loose ends to tie up."}, {"start": 1182.0, "text": "Consider this updateGoalAchievements method in ActivityItemView."}, {"start": 1187.0, "text": "It directly updates progress values as people complete activities."}, {"start": 1191.0, "text": "It can throw errors."}, {"start": 1193.0, "text": "I'll capture those errors in a state variable."}, {"start": 1196.0, "text": "I'll also pass the error to my telemetry system"}, {"start": 1200.0, "text": "so that I can improve the app in the future."}, {"start": 1203.0, "text": "And when reasonable, I'll present an alert letting people know"}, {"start": 1207.0, "text": "how to recover from the error."}, {"start": 1209.0, "text": "That's an error case handled."}, {"start": 1212.0, "text": "There are also a couple of places in the UI where state can go stale."}, {"start": 1216.0, "text": "Earlier, we removed the didSet observers that set dateEdited."}, {"start": 1220.0, "text": "And we'll want to add back that behavior now."}, {"start": 1223.0, "text": "Here's the bug:"}, {"start": 1225.0, "text": "let's say I want to sort my activities for another trip by dateEdited."}, {"start": 1230.0, "text": "In the sort dropdown I'll select \"Date Edited.\""}, {"start": 1233.0, "text": "Then if I check off this activity called \"Meditate under a tree,\""}, {"start": 1237.0, "text": "it should move to the top of the list"}, {"start": 1239.0, "text": "because I just updated one of its properties."}, {"start": 1242.0, "text": "But it just sits there."}, {"start": 1244.0, "text": "No errors are thrown here in terms of the persistence layer,"}, {"start": 1248.0, "text": "but still something isn't right."}, {"start": 1251.0, "text": "Now that we've implemented persistence,"}, {"start": 1253.0, "text": "we'll re-enable real-time updates to the dateEdited property"}, {"start": 1257.0, "text": "using the Continuous Observation feature"}, {"start": 1260.0, "text": "added to the Observation framework in the 2027 releases."}, {"start": 1265.0, "text": "In the initializer of ActivityItemView,"}, {"start": 1268.0, "text": "set up an observer using the new withContinuousObservation function."}, {"start": 1273.0, "text": "This view is where people can edit activities,"}, {"start": 1276.0, "text": "so it's a good place for the observation."}, {"start": 1279.0, "text": "Whenever someone changes either isComplete or the name of an Activity,"}, {"start": 1283.0, "text": "the observation framework will run this code to set dateEdited"}, {"start": 1287.0, "text": "to the current time,"}, {"start": 1288.0, "text": "triggering our query to automatically update the list of activities."}, {"start": 1293.0, "text": "This is also a natural place to add a side effect on Trip."}, {"start": 1297.0, "text": "Whenever an activity's status is toggled, or an activity is added or removed,"}, {"start": 1302.0, "text": "we update the isComplete property for the trip as a whole."}, {"start": 1307.0, "text": "Now you know what it takes to integrate Apple's declarative persistence framework"}, {"start": 1311.0, "text": "into your own apps."}, {"start": 1312.0, "text": "Start by considering the appropriate representation of your app's state,"}, {"start": 1317.0, "text": "and declare the Model types that make up your schema."}, {"start": 1320.0, "text": "From there, write targeted queries using predicates to balance between memory use"}, {"start": 1325.0, "text": "and on-disk storage."}, {"start": 1328.0, "text": "And stay up to date on how you can continue to adjust your SwiftUI views"}, {"start": 1332.0, "text": "for optimal interoperability with SwiftData."}, {"start": 1335.0, "text": "And with that, I can check off today's last activity,"}, {"start": 1340.0, "text": "and earn my badge."}, {"start": 1344.0, "text": "Thanks for listening, and safe travels."}]}