-
Tutorial de programación: Agrega persistencia con SwiftData
Descubre cómo funciona SwiftData mientras agregamos persistencia a una app ya existente. Te mostraremos cómo definir tus modelos de datos e integrar datos persistentes a la perfección con SwiftUI. También aprenderás las habilidades básicas para administrar el estado de tu app usando esta API expresiva y declarativa.
Capítulos
- 0:00 - Introduction
- 1:05 - Identify relevant state
- 3:17 - Define your schemas
- 9:41 - Define model relationships
- 13:33 - Update the view layer
- 21:47 - Next steps
Recursos
Videos relacionados
WWDC26
WWDC25
-
Buscar este video…
-
-
3:39 - Convert Activity to a persistent model with @Model
import Foundation import SwiftData // SwiftData automatically generates Observable conformance @Model class Activity { var name: String var isComplete: Bool = false var dateCreated = Date.now var dateEdited = Date.now } -
6:06 - Add Codable conformance to TripCollection
enum TripCollection: String, CaseIterable, RawRepresentable, Codable { case springEscapes case summerVibes case fallGetaways case winterRetreats } -
10:32 - Set up model relationships between Trip, TripImage, and Activity
import Foundation import SwiftData @Model class Trip { var name: String var collection: TripCollection var photo: TripImage var thumbnailData: Data? @Relationship(deleteRule: .cascade, inverse: \Activity.trip) var activities: [Activity] = [] private(set) var creationDate = Date.now var subtitle: String? var isComplete: Bool = false } -
13:21 - Enable interoperability between your schema and SwiftUI views
import SwiftUI import SwiftData @main struct WishlistApp: App { let container: ModelContainer = { do { let modelContainer = try ModelContainer(for: Trip.self, Activity.self, TripImage.self, Goal.self, TripGoal.self, ActivityGoal.self) try SampleData.seedIfNeeded(in: modelContainer.mainContext) return modelContainer } catch { fatalError("Could not create model container: \(error)") } }() var body: some Scene { WindowGroup { ContentView() .preferredColorScheme(.dark) } .modelContainer(container) } } -
16:27 - Fetch achieved and upcoming goals
@Query(filter: #Predicate<Goal> { $0.isAchieved }, sort: \Goal.dateAchieved, order: .reverse) private var achievedGoals: [Goal] @Query(filter: #Predicate<Goal> { !$0.isAchieved }, sort: \Goal.sortOrder) private var upcomingGoals: [Goal] -
16:49 - Fetch recent trips
import SwiftUI import SwiftData struct RecentTripsPageView: View { // Fetch most recent trips in reverse chronological order @Query(FetchDescriptor<Trip>(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 5)) private var trips: [Trip] @Namespace private var namespace var body: some View { TabView { ForEach(trips) { trip in NavigationLink { TripDetailView(trip: trip) .navigationTransition( .zoom(sourceID: trip.id, in: namespace)) } label: { TripImageView(trip: trip) .overlay(alignment: .bottomLeading) { VStack(alignment: .leading) { Text("RECENTLY ADDED") .font(.subheadline) .fontWeight(.bold) .foregroundStyle(.limeGreen) Text(trip.name) .font(.title) .fontWidth(.expanded) .fontWeight(.medium) .foregroundStyle(.primary) } .padding(.horizontal) .padding(.bottom, 54) } .matchedTransitionSource(id: trip.id, in: namespace) } .buttonStyle(.plain) } } .tabViewStyle(.page) .containerRelativeFrame([.horizontal, .vertical]) { length, axis in if axis == .vertical { return length / 1.3 } else { return length } } } } -
17:26 - Dynamically construct a query in the initializer of TripCollectionView
init(tripCollection: TripCollection, cardSize: TripCard.Size, namespace: Namespace.ID) { _trips = Query(filter: #Predicate<Trip> { $0.collection == tripCollection }, sort: \Trip.name) self.tripCollection = tripCollection self.cardSize = cardSize self.namespace = namespace } -
18:13 - Search for trips and activities by name
import SwiftUI import SwiftData private struct SearchResultsListView: View { @Query(sort: \Trip.name) private var trips: [Trip] @Query(sort: \Activity.name) private var activities: [Activity] var searchText: String var namespace: Namespace.ID init(searchText: String, namespace: Namespace.ID) { self.searchText = searchText self.namespace = namespace if searchText.isEmpty { _trips = Query(FetchDescriptor(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 3)) _activities = Query(filter: #Predicate<Activity> { _ in false }) } else { // All trips whose name matches searchText, sorted lexicographically let tripSearchPredicate = #Predicate<Trip> { $0.name.localizedStandardContains(searchText) } _trips = Query(filter: tripSearchPredicate, sort: \Trip.name) // All matching activities that belong to a trip let activitySearchPredicate = #Predicate<Activity> { $0.trip != nil && $0.name.localizedStandardContains(searchText) } _activities = Query(filter: activitySearchPredicate, sort: \Activity.name) } } var body: some View { List { if !trips.isEmpty { TripSearchSectionView(trips: trips, namespace: namespace, title: searchText.isEmpty ? "Recent Trips" : "Trips") } if !activities.isEmpty { ActivitySearchSectionView(activities: activities) } } .overlay { if trips.isEmpty && activities.isEmpty { ContentUnavailableView( "No results for “\(searchText)”", systemImage: "magnifyingglass", description: Text("Check spelling or try a new search.") ) } } .listStyle(.plain) } } -
19:42 - Capture and report errors from ActivityItemView
var body: some View { HStack(alignment: .firstTextBaseline, spacing: 17) { Group { if isEditing { rowContentWhenEditing } else { rowContentWhenNotEditing } } .transition(.opacity.animation(.snappy)) .animation(.snappy, value: isEditing) } .onDisappear { do { try updateGoalAchievements() } catch { updateError = error reportError(error) } } .alert(error: $updateError) { // Customize the presentation of the error } } -
21:04 - Update dateEdited and propagate side effects on property changes
init(activity: Activity, isLast: Bool, isEditing: Bool) { activity.token = withContinuousObservation(options: .didSet) { event in _ = activity.name _ = activity.isComplete if event.matches(\Activity.name) { activity.dateEdited = .now } if event.matches(\Activity.isComplete) { activity.dateEdited = .now activity.trip?.isComplete = activity.trip?.activities.isEmpty == false && activity.trip?.activities.allSatisfy { $0.isComplete } == true } } self.activity = activity self.isLast = isLast self.isEditing = isEditing }
-
-
- 0:00 - Introduction
An introduction to the Wishlist sample app and the three steps for adopting SwiftData: identifying relevant state, defining schemas, and defining model relationships.
- 1:05 - Identify relevant state
Identify the data types and variables in Wishlist — trip collections, goal statuses, and the DataSource — that will become SwiftData models connected through a ModelContext.
- 3:17 - Define your schemas
Convert Activity, Trip, and Goal into @Model types. Covers handling property observers with the @Model macro, refactoring the Goal enumeration into a class hierarchy using inheritance with TripGoal and ActivityGoal subclasses, and inlining thumbnail data.
- 9:41 - Define model relationships
Declare to-many relationships between Trip and Activity using the @Relationship macro, remove the now-redundant DataSource and TripEditModel helpers, and attach the modelContainer scene modifier to complete the model layer.
- 13:33 - Update the view layer
Replace environment DataSource properties with @Query macros and targeted FetchDescriptor predicates in each subview. Covers autosave, surfacing runtime errors with SwiftUI view modifiers, and re-enabling dateEdited property observers using the new withContinuousObservation API.
- 21:47 - Next steps
Key takeaways: design a schema that fits your data model, balance memory and disk usage with targeted queries, and plan for interoperability and extensibility as your app evolves.